affiliations
This commit is contained in:
1 parent
4f3f9c9aad
commit
0b5b3dd3ee
177 files changed
+9660
-8531
No files matched your search
@@ -0,0 +1,541 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\ClubFacility;
|
||||
use App\Models\ClubInstructor;
|
||||
use App\Models\ClubActivity;
|
||||
use App\Models\ClubPackage;
|
||||
use App\Models\Membership;
|
||||
use App\Models\ClubGalleryImage;
|
||||
use App\Models\ClubTransaction;
|
||||
use App\Models\ClubMessage;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ClubAdminController extends Controller
|
||||
{
|
||||
/**
|
||||
* Get club and verify access
|
||||
*/
|
||||
private function getClub($clubId)
|
||||
{
|
||||
$club = Tenant::findOrFail($clubId);
|
||||
|
||||
// TODO: Add proper authorization check
|
||||
// For now, allow super-admin or club owner
|
||||
$user = Auth::user();
|
||||
if (!$user->isSuperAdmin() && $club->owner_user_id !== $user->id) {
|
||||
abort(403, 'Unauthorized access to this club.');
|
||||
}
|
||||
|
||||
return $club;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard overview
|
||||
*/
|
||||
public function dashboard($clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
|
||||
$stats = [
|
||||
'members' => Membership::where('tenant_id', $clubId)->where('status', 'active')->count(),
|
||||
'activities' => ClubActivity::where('tenant_id', $clubId)->count(),
|
||||
'packages' => ClubPackage::where('tenant_id', $clubId)->count(),
|
||||
'instructors' => ClubInstructor::where('tenant_id', $clubId)->count(),
|
||||
'rating' => $club->reviews()->avg('rating') ?? 0,
|
||||
];
|
||||
|
||||
// Monthly financial data for chart
|
||||
$monthlyFinancials = $this->getMonthlyFinancials($clubId);
|
||||
|
||||
// Expiring subscriptions (next 30 days)
|
||||
$expiringSubscriptions = collect(); // TODO: Implement when subscription model is ready
|
||||
|
||||
return view('admin.club.dashboard', compact('club', 'stats', 'monthlyFinancials', 'expiringSubscriptions'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Club details
|
||||
*/
|
||||
public function details($clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
return view('admin.club.details', compact('club'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gallery management
|
||||
*/
|
||||
public function gallery($clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
$images = ClubGalleryImage::where('tenant_id', $clubId)->latest()->get();
|
||||
return view('admin.club.gallery', compact('club', 'images'));
|
||||
}
|
||||
|
||||
public function uploadGallery(Request $request, $clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
|
||||
$request->validate([
|
||||
'images.*' => 'required|image|max:5120',
|
||||
'caption' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
if ($request->hasFile('images')) {
|
||||
foreach ($request->file('images') as $image) {
|
||||
$path = $image->store('clubs/' . $clubId . '/gallery', 'public');
|
||||
ClubGalleryImage::create([
|
||||
'tenant_id' => $clubId,
|
||||
'path' => $path,
|
||||
'caption' => $request->caption,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return back()->with('success', 'Images uploaded successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Facilities management
|
||||
*/
|
||||
public function facilities($clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
$facilities = ClubFacility::where('tenant_id', $clubId)->get();
|
||||
return view('admin.club.facilities', compact('club', 'facilities'));
|
||||
}
|
||||
|
||||
public function storeFacility(Request $request, $clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'image' => 'nullable',
|
||||
]);
|
||||
|
||||
$data = $request->only(['name', 'description']);
|
||||
$data['tenant_id'] = $clubId;
|
||||
|
||||
// Handle base64 image from cropper (form mode)
|
||||
if ($request->filled('image') && str_starts_with($request->image, 'data:image')) {
|
||||
$imageData = $request->image;
|
||||
$imageParts = explode(";base64,", $imageData);
|
||||
$imageTypeAux = explode("image/", $imageParts[0]);
|
||||
$extension = $imageTypeAux[1];
|
||||
$imageBinary = base64_decode($imageParts[1]);
|
||||
|
||||
$folder = $request->input('image_folder', 'clubs/' . $clubId . '/facilities');
|
||||
$filename = $request->input('image_filename', 'facility_' . time());
|
||||
$fullPath = $folder . '/' . $filename . '.' . $extension;
|
||||
|
||||
Storage::disk('public')->put($fullPath, $imageBinary);
|
||||
$data['image'] = $fullPath;
|
||||
}
|
||||
// Handle traditional file upload
|
||||
elseif ($request->hasFile('image')) {
|
||||
$data['image'] = $request->file('image')->store('clubs/' . $clubId . '/facilities', 'public');
|
||||
}
|
||||
|
||||
ClubFacility::create($data);
|
||||
|
||||
return back()->with('success', 'Facility added successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Instructors management
|
||||
*/
|
||||
public function instructors($clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
$instructors = ClubInstructor::where('tenant_id', $clubId)->get();
|
||||
return view('admin.club.instructors', compact('club', 'instructors'));
|
||||
}
|
||||
|
||||
public function storeInstructor(Request $request, $clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'specialization' => 'nullable|string|max:255',
|
||||
'bio' => 'nullable|string',
|
||||
'photo' => 'nullable',
|
||||
]);
|
||||
|
||||
$data = $request->only(['name', 'specialization', 'bio']);
|
||||
$data['tenant_id'] = $clubId;
|
||||
|
||||
// Handle base64 image from cropper (form mode)
|
||||
if ($request->filled('photo') && str_starts_with($request->photo, 'data:image')) {
|
||||
$imageData = $request->photo;
|
||||
$imageParts = explode(";base64,", $imageData);
|
||||
$imageTypeAux = explode("image/", $imageParts[0]);
|
||||
$extension = $imageTypeAux[1];
|
||||
$imageBinary = base64_decode($imageParts[1]);
|
||||
|
||||
$folder = $request->input('photo_folder', 'clubs/' . $clubId . '/instructors');
|
||||
$filename = $request->input('photo_filename', 'instructor_' . time());
|
||||
$fullPath = $folder . '/' . $filename . '.' . $extension;
|
||||
|
||||
Storage::disk('public')->put($fullPath, $imageBinary);
|
||||
$data['photo'] = $fullPath;
|
||||
}
|
||||
// Handle traditional file upload
|
||||
elseif ($request->hasFile('photo')) {
|
||||
$data['photo'] = $request->file('photo')->store('clubs/' . $clubId . '/instructors', 'public');
|
||||
}
|
||||
|
||||
ClubInstructor::create($data);
|
||||
|
||||
return back()->with('success', 'Instructor added successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Activities management
|
||||
*/
|
||||
public function activities($clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
$activities = ClubActivity::where('tenant_id', $clubId)->with('instructor')->get();
|
||||
$instructors = ClubInstructor::where('tenant_id', $clubId)->get();
|
||||
return view('admin.club.activities', compact('club', 'activities', 'instructors'));
|
||||
}
|
||||
|
||||
public function storeActivity(Request $request, $clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'duration' => 'nullable|integer',
|
||||
'instructor_id' => 'nullable|exists:club_instructors,id',
|
||||
]);
|
||||
|
||||
$data = $request->only(['name', 'description', 'duration', 'instructor_id']);
|
||||
$data['tenant_id'] = $clubId;
|
||||
|
||||
ClubActivity::create($data);
|
||||
|
||||
return back()->with('success', 'Activity added successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Packages management
|
||||
*/
|
||||
public function packages($clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
$packages = ClubPackage::where('tenant_id', $clubId)->get();
|
||||
return view('admin.club.packages', compact('club', 'packages'));
|
||||
}
|
||||
|
||||
public function storePackage(Request $request, $clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'price' => 'required|numeric|min:0',
|
||||
'duration_days' => 'required|integer|min:1',
|
||||
'is_popular' => 'boolean',
|
||||
]);
|
||||
|
||||
$data = $request->only(['name', 'description', 'price', 'duration_days']);
|
||||
$data['tenant_id'] = $clubId;
|
||||
$data['is_popular'] = $request->boolean('is_popular');
|
||||
|
||||
ClubPackage::create($data);
|
||||
|
||||
return back()->with('success', 'Package added successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Members management
|
||||
*/
|
||||
public function members($clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
$members = Membership::where('tenant_id', $clubId)
|
||||
->with(['user', 'subscription.package'])
|
||||
->paginate(20);
|
||||
$packages = ClubPackage::where('tenant_id', $clubId)->get();
|
||||
return view('admin.club.members', compact('club', 'members', 'packages'));
|
||||
}
|
||||
|
||||
public function storeMember(Request $request, $clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
|
||||
// TODO: Implement member creation logic
|
||||
|
||||
return back()->with('success', 'Member added successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Roles management
|
||||
*/
|
||||
public function roles($clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
$staffMembers = collect(); // TODO: Implement staff roles
|
||||
return view('admin.club.roles', compact('club', 'staffMembers'));
|
||||
}
|
||||
|
||||
public function storeRole(Request $request, $clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
|
||||
// TODO: Implement role assignment
|
||||
|
||||
return back()->with('success', 'Role assigned successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Financials management
|
||||
*/
|
||||
public function financials($clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
$transactions = ClubTransaction::where('tenant_id', $clubId)
|
||||
->latest('transaction_date')
|
||||
->paginate(20);
|
||||
|
||||
$summary = [
|
||||
'total_income' => ClubTransaction::where('tenant_id', $clubId)
|
||||
->where('type', 'income')
|
||||
->sum('amount'),
|
||||
'total_expenses' => ClubTransaction::where('tenant_id', $clubId)
|
||||
->where('type', 'expense')
|
||||
->sum('amount'),
|
||||
'net_profit' => 0,
|
||||
'pending' => ClubTransaction::where('tenant_id', $clubId)
|
||||
->where('status', 'pending')
|
||||
->sum('amount'),
|
||||
];
|
||||
$summary['net_profit'] = $summary['total_income'] - $summary['total_expenses'];
|
||||
|
||||
return view('admin.club.financials', compact('club', 'transactions', 'summary'));
|
||||
}
|
||||
|
||||
public function storeIncome(Request $request, $clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
|
||||
$request->validate([
|
||||
'description' => 'required|string|max:255',
|
||||
'amount' => 'required|numeric|min:0',
|
||||
'transaction_date' => 'required|date',
|
||||
]);
|
||||
|
||||
ClubTransaction::create([
|
||||
'tenant_id' => $clubId,
|
||||
'description' => $request->description,
|
||||
'amount' => $request->amount,
|
||||
'type' => 'income',
|
||||
'transaction_date' => $request->transaction_date,
|
||||
'status' => 'paid',
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Income recorded successfully.');
|
||||
}
|
||||
|
||||
public function storeExpense(Request $request, $clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
|
||||
$request->validate([
|
||||
'description' => 'required|string|max:255',
|
||||
'amount' => 'required|numeric|min:0',
|
||||
'transaction_date' => 'required|date',
|
||||
]);
|
||||
|
||||
ClubTransaction::create([
|
||||
'tenant_id' => $clubId,
|
||||
'description' => $request->description,
|
||||
'amount' => $request->amount,
|
||||
'type' => 'expense',
|
||||
'transaction_date' => $request->transaction_date,
|
||||
'status' => 'paid',
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Expense recorded successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Messages management
|
||||
*/
|
||||
public function messages($clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
$conversations = collect(); // TODO: Implement messaging
|
||||
$members = Membership::where('tenant_id', $clubId)->with('user')->get();
|
||||
return view('admin.club.messages', compact('club', 'conversations', 'members'));
|
||||
}
|
||||
|
||||
public function sendMessage(Request $request, $clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
|
||||
// TODO: Implement message sending
|
||||
|
||||
return back()->with('success', 'Message sent successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Analytics
|
||||
*/
|
||||
public function analytics($clubId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
|
||||
$analytics = [
|
||||
'new_members' => 0,
|
||||
'new_members_change' => 0,
|
||||
'retention_rate' => 0,
|
||||
'retention_change' => 0,
|
||||
'avg_revenue' => 0,
|
||||
'total_checkins' => 0,
|
||||
'checkins_change' => 0,
|
||||
'monthly_members' => array_fill(0, 12, 0),
|
||||
'activity_labels' => ['No data'],
|
||||
'activity_data' => [100],
|
||||
'hourly_checkins' => array_fill(0, 9, 0),
|
||||
];
|
||||
|
||||
$popularPackages = ClubPackage::where('tenant_id', $clubId)
|
||||
->withCount('subscriptions')
|
||||
->orderByDesc('subscriptions_count')
|
||||
->take(5)
|
||||
->get();
|
||||
|
||||
return view('admin.club.analytics', compact('club', 'analytics', 'popularPackages'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Get monthly financial data for charts
|
||||
*/
|
||||
private function getMonthlyFinancials($clubId)
|
||||
{
|
||||
$months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
$data = [];
|
||||
|
||||
for ($i = 0; $i < 12; $i++) {
|
||||
$data[] = [
|
||||
'month' => $months[$i],
|
||||
'income' => 0,
|
||||
'expenses' => 0,
|
||||
'profit' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
// TODO: Populate with actual transaction data
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload facility image via AJAX (cropper).
|
||||
*/
|
||||
public function uploadFacilityImage(Request $request, $clubId, $facilityId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
|
||||
$request->validate([
|
||||
'image' => 'required',
|
||||
'folder' => 'required|string',
|
||||
'filename' => 'required|string',
|
||||
]);
|
||||
|
||||
try {
|
||||
$facility = ClubFacility::where('tenant_id', $clubId)->findOrFail($facilityId);
|
||||
|
||||
// Handle base64 image from cropper
|
||||
$imageData = $request->image;
|
||||
$imageParts = explode(";base64,", $imageData);
|
||||
$imageTypeAux = explode("image/", $imageParts[0]);
|
||||
$extension = $imageTypeAux[1];
|
||||
$imageBinary = base64_decode($imageParts[1]);
|
||||
|
||||
$folder = trim($request->folder, '/');
|
||||
$fileName = $request->filename . '.' . $extension;
|
||||
$fullPath = $folder . '/' . $fileName;
|
||||
|
||||
// Delete old image if exists
|
||||
if ($facility->image && Storage::disk('public')->exists($facility->image)) {
|
||||
Storage::disk('public')->delete($facility->image);
|
||||
}
|
||||
|
||||
// Store in the public disk
|
||||
Storage::disk('public')->put($fullPath, $imageBinary);
|
||||
|
||||
// Update facility's image field
|
||||
$facility->update(['image' => $fullPath]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'path' => $fullPath,
|
||||
'url' => asset('storage/' . $fullPath)
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload instructor photo via AJAX (cropper).
|
||||
*/
|
||||
public function uploadInstructorPhoto(Request $request, $clubId, $instructorId)
|
||||
{
|
||||
$club = $this->getClub($clubId);
|
||||
|
||||
$request->validate([
|
||||
'image' => 'required',
|
||||
'folder' => 'required|string',
|
||||
'filename' => 'required|string',
|
||||
]);
|
||||
|
||||
try {
|
||||
$instructor = ClubInstructor::where('tenant_id', $clubId)->findOrFail($instructorId);
|
||||
|
||||
// Handle base64 image from cropper
|
||||
$imageData = $request->image;
|
||||
$imageParts = explode(";base64,", $imageData);
|
||||
$imageTypeAux = explode("image/", $imageParts[0]);
|
||||
$extension = $imageTypeAux[1];
|
||||
$imageBinary = base64_decode($imageParts[1]);
|
||||
|
||||
$folder = trim($request->folder, '/');
|
||||
$fileName = $request->filename . '.' . $extension;
|
||||
$fullPath = $folder . '/' . $fileName;
|
||||
|
||||
// Delete old photo if exists
|
||||
if ($instructor->photo && Storage::disk('public')->exists($instructor->photo)) {
|
||||
Storage::disk('public')->delete($instructor->photo);
|
||||
}
|
||||
|
||||
// Store in the public disk
|
||||
Storage::disk('public')->put($fullPath, $imageBinary);
|
||||
|
||||
// Update instructor's photo field
|
||||
$instructor->update(['photo' => $fullPath]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'path' => $fullPath,
|
||||
'url' => asset('storage/' . $fullPath)
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,8 +105,8 @@ class PlatformController extends Controller
|
||||
'address' => 'nullable|string',
|
||||
'gps_lat' => 'nullable|numeric|between:-90,90',
|
||||
'gps_long' => 'nullable|numeric|between:-180,180',
|
||||
'logo' => 'nullable|image|max:2048',
|
||||
'cover_image' => 'nullable|image|max:2048',
|
||||
'logo' => 'nullable',
|
||||
'cover_image' => 'nullable',
|
||||
]);
|
||||
|
||||
// Handle phone as JSON
|
||||
@@ -117,14 +117,48 @@ class PlatformController extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
// Handle logo upload
|
||||
if ($request->hasFile('logo')) {
|
||||
// Handle logo - base64 from cropper (form mode)
|
||||
if ($request->filled('logo') && str_starts_with($request->logo, 'data:image')) {
|
||||
$imageData = $request->logo;
|
||||
$imageParts = explode(";base64,", $imageData);
|
||||
$imageTypeAux = explode("image/", $imageParts[0]);
|
||||
$extension = $imageTypeAux[1];
|
||||
$imageBinary = base64_decode($imageParts[1]);
|
||||
|
||||
$folder = $request->input('logo_folder', 'clubs/logos');
|
||||
$filename = $request->input('logo_filename', 'logo_' . time());
|
||||
$fullPath = $folder . '/' . $filename . '.' . $extension;
|
||||
|
||||
Storage::disk('public')->put($fullPath, $imageBinary);
|
||||
$validated['logo'] = $fullPath;
|
||||
}
|
||||
// Handle logo - traditional file upload
|
||||
elseif ($request->hasFile('logo')) {
|
||||
$validated['logo'] = $request->file('logo')->store('clubs/logos', 'public');
|
||||
} else {
|
||||
unset($validated['logo']);
|
||||
}
|
||||
|
||||
// Handle cover image upload
|
||||
if ($request->hasFile('cover_image')) {
|
||||
// Handle cover image - base64 from cropper (form mode)
|
||||
if ($request->filled('cover_image') && str_starts_with($request->cover_image, 'data:image')) {
|
||||
$imageData = $request->cover_image;
|
||||
$imageParts = explode(";base64,", $imageData);
|
||||
$imageTypeAux = explode("image/", $imageParts[0]);
|
||||
$extension = $imageTypeAux[1];
|
||||
$imageBinary = base64_decode($imageParts[1]);
|
||||
|
||||
$folder = $request->input('cover_image_folder', 'clubs/covers');
|
||||
$filename = $request->input('cover_image_filename', 'cover_' . time());
|
||||
$fullPath = $folder . '/' . $filename . '.' . $extension;
|
||||
|
||||
Storage::disk('public')->put($fullPath, $imageBinary);
|
||||
$validated['cover_image'] = $fullPath;
|
||||
}
|
||||
// Handle cover image - traditional file upload
|
||||
elseif ($request->hasFile('cover_image')) {
|
||||
$validated['cover_image'] = $request->file('cover_image')->store('clubs/covers', 'public');
|
||||
} else {
|
||||
unset($validated['cover_image']);
|
||||
}
|
||||
|
||||
$club = Tenant::create($validated);
|
||||
@@ -355,4 +389,92 @@ class PlatformController extends Controller
|
||||
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload club logo via AJAX (cropper).
|
||||
*/
|
||||
public function uploadClubLogo(Request $request, Tenant $club)
|
||||
{
|
||||
$request->validate([
|
||||
'image' => 'required',
|
||||
'folder' => 'required|string',
|
||||
'filename' => 'required|string',
|
||||
]);
|
||||
|
||||
try {
|
||||
// Handle base64 image from cropper
|
||||
$imageData = $request->image;
|
||||
$imageParts = explode(";base64,", $imageData);
|
||||
$imageTypeAux = explode("image/", $imageParts[0]);
|
||||
$extension = $imageTypeAux[1];
|
||||
$imageBinary = base64_decode($imageParts[1]);
|
||||
|
||||
$folder = trim($request->folder, '/');
|
||||
$fileName = $request->filename . '.' . $extension;
|
||||
$fullPath = $folder . '/' . $fileName;
|
||||
|
||||
// Delete old logo if exists
|
||||
if ($club->logo && Storage::disk('public')->exists($club->logo)) {
|
||||
Storage::disk('public')->delete($club->logo);
|
||||
}
|
||||
|
||||
// Store in the public disk
|
||||
Storage::disk('public')->put($fullPath, $imageBinary);
|
||||
|
||||
// Update club's logo field
|
||||
$club->update(['logo' => $fullPath]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'path' => $fullPath,
|
||||
'url' => asset('storage/' . $fullPath)
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload club cover image via AJAX (cropper).
|
||||
*/
|
||||
public function uploadClubCover(Request $request, Tenant $club)
|
||||
{
|
||||
$request->validate([
|
||||
'image' => 'required',
|
||||
'folder' => 'required|string',
|
||||
'filename' => 'required|string',
|
||||
]);
|
||||
|
||||
try {
|
||||
// Handle base64 image from cropper
|
||||
$imageData = $request->image;
|
||||
$imageParts = explode(";base64,", $imageData);
|
||||
$imageTypeAux = explode("image/", $imageParts[0]);
|
||||
$extension = $imageTypeAux[1];
|
||||
$imageBinary = base64_decode($imageParts[1]);
|
||||
|
||||
$folder = trim($request->folder, '/');
|
||||
$fileName = $request->filename . '.' . $extension;
|
||||
$fullPath = $folder . '/' . $fileName;
|
||||
|
||||
// Delete old cover if exists
|
||||
if ($club->cover_image && Storage::disk('public')->exists($club->cover_image)) {
|
||||
Storage::disk('public')->delete($club->cover_image);
|
||||
}
|
||||
|
||||
// Store in the public disk
|
||||
Storage::disk('public')->put($fullPath, $imageBinary);
|
||||
|
||||
// Update club's cover_image field
|
||||
$club->update(['cover_image' => $fullPath]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'path' => $fullPath,
|
||||
'url' => asset('storage/' . $fullPath)
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ use App\Models\ClubAffiliation;
|
||||
use App\Services\FamilyService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class FamilyController extends Controller
|
||||
{
|
||||
@@ -89,9 +90,17 @@ class FamilyController extends Controller
|
||||
$totalSessions = $attendanceRecords->count();
|
||||
$attendanceRate = $totalSessions > 0 ? round(($sessionsCompleted / $totalSessions) * 100, 1) : 0;
|
||||
|
||||
// Fetch affiliations data
|
||||
// Fetch affiliations data with enhanced relationships
|
||||
$clubAffiliations = $user->clubAffiliations()
|
||||
->with(['skillAcquisitions', 'affiliationMedia'])
|
||||
->with([
|
||||
'skillAcquisitions.package',
|
||||
'skillAcquisitions.activity',
|
||||
'skillAcquisitions.instructor.user',
|
||||
'affiliationMedia',
|
||||
'subscriptions.package.activities',
|
||||
'subscriptions.package.packageActivities.activity',
|
||||
'subscriptions.package.packageActivities.instructor.user',
|
||||
])
|
||||
->orderBy('start_date', 'desc')
|
||||
->get();
|
||||
|
||||
@@ -107,6 +116,16 @@ class FamilyController extends Controller
|
||||
$distinctSkills = $clubAffiliations->flatMap->skillAcquisitions->pluck('skill_name')->unique()->count();
|
||||
$totalMembershipDuration = $clubAffiliations->sum('duration_in_months');
|
||||
|
||||
// Get all unique skills for filter dropdown
|
||||
$allSkills = $clubAffiliations->flatMap(function($affiliation) {
|
||||
return $affiliation->skillAcquisitions->pluck('skill_name');
|
||||
})->unique()->sort()->values();
|
||||
|
||||
// Count total instructors
|
||||
$totalInstructors = $clubAffiliations->flatMap(function($affiliation) {
|
||||
return $affiliation->skillAcquisitions->pluck('instructor');
|
||||
})->filter()->unique('id')->count();
|
||||
|
||||
// Pass user directly and a flag to indicate it's the current user's profile
|
||||
return view('family.show', [
|
||||
'relationship' => (object)[
|
||||
@@ -134,6 +153,9 @@ class FamilyController extends Controller
|
||||
'totalAffiliations' => $totalAffiliations,
|
||||
'distinctSkills' => $distinctSkills,
|
||||
'totalMembershipDuration' => $totalMembershipDuration,
|
||||
'allSkills' => $allSkills,
|
||||
'totalInstructors' => $totalInstructors,
|
||||
'user' => $user,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -177,16 +199,36 @@ class FamilyController extends Controller
|
||||
$fileName = $request->filename . '.' . $extension;
|
||||
$fullPath = $folder . '/' . $fileName;
|
||||
|
||||
// Use public/storage directory directly (not through Storage facade)
|
||||
$publicStoragePath = public_path('storage');
|
||||
|
||||
// Delete old profile picture if exists
|
||||
if ($user->profile_picture && \Storage::disk('public')->exists($user->profile_picture)) {
|
||||
\Storage::disk('public')->delete($user->profile_picture);
|
||||
$oldPath = $user->profile_picture;
|
||||
if ($oldPath && file_exists($publicStoragePath . '/' . $oldPath)) {
|
||||
unlink($publicStoragePath . '/' . $oldPath);
|
||||
}
|
||||
|
||||
// Store in the public disk (storage/app/public)
|
||||
\Storage::disk('public')->put($fullPath, $imageBinary);
|
||||
// Also delete any existing files with different extensions
|
||||
$basePath = $folder . '/' . $request->filename;
|
||||
foreach (['png', 'jpg', 'jpeg', 'webp', 'gif'] as $ext) {
|
||||
$checkPath = $publicStoragePath . '/' . $basePath . '.' . $ext;
|
||||
if (file_exists($checkPath)) {
|
||||
unlink($checkPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Update user's profile_picture field
|
||||
$user->update(['profile_picture' => $fullPath]);
|
||||
// Ensure directory exists
|
||||
$fullDir = $publicStoragePath . '/' . $folder;
|
||||
if (!is_dir($fullDir)) {
|
||||
mkdir($fullDir, 0755, true);
|
||||
}
|
||||
|
||||
// Save file directly to public/storage
|
||||
file_put_contents($publicStoragePath . '/' . $fullPath, $imageBinary);
|
||||
|
||||
// Update user's profile_picture field (use save() for reliable persistence)
|
||||
$user->profile_picture = $fullPath;
|
||||
$user->save();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
@@ -336,9 +378,17 @@ class FamilyController extends Controller
|
||||
$totalSessions = $attendanceRecords->count();
|
||||
$attendanceRate = $totalSessions > 0 ? round(($sessionsCompleted / $totalSessions) * 100, 1) : 0;
|
||||
|
||||
// Fetch affiliations data for the dependent
|
||||
// Fetch affiliations data for the dependent with enhanced relationships
|
||||
$clubAffiliations = $relationship->dependent->clubAffiliations()
|
||||
->with(['skillAcquisitions', 'affiliationMedia'])
|
||||
->with([
|
||||
'skillAcquisitions.package',
|
||||
'skillAcquisitions.activity',
|
||||
'skillAcquisitions.instructor.user',
|
||||
'affiliationMedia',
|
||||
'subscriptions.package.activities',
|
||||
'subscriptions.package.packageActivities.activity',
|
||||
'subscriptions.package.packageActivities.instructor.user',
|
||||
])
|
||||
->orderBy('start_date', 'desc')
|
||||
->get();
|
||||
|
||||
@@ -354,6 +404,16 @@ class FamilyController extends Controller
|
||||
$distinctSkills = $clubAffiliations->flatMap->skillAcquisitions->pluck('skill_name')->unique()->count();
|
||||
$totalMembershipDuration = $clubAffiliations->sum('duration_in_months');
|
||||
|
||||
// Get all unique skills for filter dropdown
|
||||
$allSkills = $clubAffiliations->flatMap(function($affiliation) {
|
||||
return $affiliation->skillAcquisitions->pluck('skill_name');
|
||||
})->unique()->sort()->values();
|
||||
|
||||
// Count total instructors
|
||||
$totalInstructors = $clubAffiliations->flatMap(function($affiliation) {
|
||||
return $affiliation->skillAcquisitions->pluck('instructor');
|
||||
})->filter()->unique('id')->count();
|
||||
|
||||
return view('family.show', [
|
||||
'relationship' => $relationship,
|
||||
'latestHealthRecord' => $latestHealthRecord,
|
||||
@@ -375,6 +435,9 @@ class FamilyController extends Controller
|
||||
'totalAffiliations' => $totalAffiliations,
|
||||
'distinctSkills' => $distinctSkills,
|
||||
'totalMembershipDuration' => $totalMembershipDuration,
|
||||
'allSkills' => $allSkills,
|
||||
'totalInstructors' => $totalInstructors,
|
||||
'user' => $relationship->dependent,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -501,12 +564,12 @@ class FamilyController extends Controller
|
||||
$fullPath = $folder . '/' . $fileName;
|
||||
|
||||
// Delete old profile picture if exists
|
||||
if ($familyMember->profile_picture && \Storage::disk('public')->exists($familyMember->profile_picture)) {
|
||||
\Storage::disk('public')->delete($familyMember->profile_picture);
|
||||
if ($familyMember->profile_picture && Storage::disk('public')->exists($familyMember->profile_picture)) {
|
||||
Storage::disk('public')->delete($familyMember->profile_picture);
|
||||
}
|
||||
|
||||
// Store in the public disk (storage/app/public)
|
||||
\Storage::disk('public')->put($fullPath, $imageBinary);
|
||||
Storage::disk('public')->put($fullPath, $imageBinary);
|
||||
|
||||
// Update family member's profile_picture field
|
||||
$familyMember->update(['profile_picture' => $fullPath]);
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\InstructorReview;
|
||||
use App\Models\ClubInstructor;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class InstructorReviewController extends Controller
|
||||
{
|
||||
/**
|
||||
* Store a new review.
|
||||
*/
|
||||
public function store(Request $request, $instructorId)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'rating' => 'required|integer|min:1|max:5',
|
||||
'comment' => 'nullable|string|max:1000',
|
||||
]);
|
||||
|
||||
$instructor = ClubInstructor::findOrFail($instructorId);
|
||||
$user = Auth::user();
|
||||
|
||||
// Check if user already has a review
|
||||
$existingReview = InstructorReview::where('instructor_id', $instructorId)
|
||||
->where('reviewer_user_id', $user->id)
|
||||
->first();
|
||||
|
||||
if ($existingReview) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'You have already reviewed this instructor. Please edit your existing review.'
|
||||
], 400);
|
||||
}
|
||||
|
||||
$review = InstructorReview::create([
|
||||
'instructor_id' => $instructorId,
|
||||
'reviewer_user_id' => $user->id,
|
||||
'rating' => $validated['rating'],
|
||||
'comment' => $validated['comment'] ?? null,
|
||||
'reviewed_at' => now(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Review submitted successfully',
|
||||
'review' => $review->load('reviewer'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing review.
|
||||
*/
|
||||
public function update(Request $request, $reviewId)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'rating' => 'required|integer|min:1|max:5',
|
||||
'comment' => 'nullable|string|max:1000',
|
||||
]);
|
||||
|
||||
$review = InstructorReview::findOrFail($reviewId);
|
||||
$user = Auth::user();
|
||||
|
||||
// Ensure user owns this review
|
||||
if ($review->reviewer_user_id !== $user->id) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'You can only edit your own reviews.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
$review->update([
|
||||
'rating' => $validated['rating'],
|
||||
'comment' => $validated['comment'] ?? null,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Review updated successfully',
|
||||
'review' => $review->fresh()->load('reviewer'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reviews for an instructor.
|
||||
*/
|
||||
public function index($instructorId)
|
||||
{
|
||||
$instructor = ClubInstructor::findOrFail($instructorId);
|
||||
$reviews = $instructor->reviews()
|
||||
->with('reviewer')
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'reviews' => $reviews,
|
||||
'average_rating' => $instructor->average_rating,
|
||||
'total_reviews' => $instructor->reviews_count,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user