From a7434e33d77d1d15ce9dc8b0febd82da8fa700e5 Mon Sep 17 00:00:00 2001 From: GhassanYusuf Date: Wed, 28 Jan 2026 02:07:06 +0300 Subject: [PATCH] finally finished edit profile modal --- ADMIN_MEMBERS_FIX.md | 230 ++ .../Controllers/Admin/PlatformController.php | 404 +++ app/Http/Controllers/FamilyController.php | 170 +- app/Http/Controllers/MemberController.php | 709 +++++ app/Models/User.php | 2 + ...5405_add_marital_status_to_users_table.php | 28 + ...file_picture_visibility_to_users_table.php | 28 + .../views/admin/platform/members.blade.php | 14 +- .../components/edit-profile-modal.blade.php | 1005 +++++++ .../components/nationality-dropdown.blade.php | 17 +- .../components/social-link-row.blade.php | 92 + resources/views/family/edit.blade.php | 401 +-- .../partials/affiliations-enhanced.blade.php | 44 +- resources/views/family/profile-edit.blade.php | 292 +- resources/views/family/show.blade.php | 30 +- resources/views/layouts/app.blade.php | 6 +- resources/views/member/create.blade.php | 276 ++ resources/views/member/dashboard.blade.php | 256 ++ resources/views/member/edit.blade.php | 16 + resources/views/member/index.blade.php | 256 ++ .../partials/affiliations-enhanced.blade.php | 916 ++++++ resources/views/member/profile-edit.blade.php | 14 + resources/views/member/show.blade.php | 2550 +++++++++++++++++ .../takeone/components/widget.blade.php | 24 +- routes/web.php | 73 +- 25 files changed, 7088 insertions(+), 765 deletions(-) create mode 100644 ADMIN_MEMBERS_FIX.md create mode 100644 app/Http/Controllers/MemberController.php create mode 100644 database/migrations/2026_01_27_185405_add_marital_status_to_users_table.php create mode 100644 database/migrations/2026_01_27_221321_add_profile_picture_visibility_to_users_table.php create mode 100644 resources/views/components/edit-profile-modal.blade.php create mode 100644 resources/views/components/social-link-row.blade.php create mode 100644 resources/views/member/create.blade.php create mode 100644 resources/views/member/dashboard.blade.php create mode 100644 resources/views/member/edit.blade.php create mode 100644 resources/views/member/index.blade.php create mode 100644 resources/views/member/partials/affiliations-enhanced.blade.php create mode 100644 resources/views/member/profile-edit.blade.php create mode 100644 resources/views/member/show.blade.php diff --git a/ADMIN_MEMBERS_FIX.md b/ADMIN_MEMBERS_FIX.md new file mode 100644 index 0000000..e20a42f --- /dev/null +++ b/ADMIN_MEMBERS_FIX.md @@ -0,0 +1,230 @@ +# Admin Members Management - Separate Routes Implementation + +## Overview + +This implementation creates a clean separation between family member management and admin member management by introducing dedicated admin routes. This ensures: +- `/family/*` routes are ONLY for actual family members +- `/admin/members/*` routes are for admins to manage ALL platform members +- No confusion between family relationships and admin access + +## Issues Fixed + +### 1. 404 Error for Non-Family Members (View Profile) +**Problem**: When clicking on member cards in the admin dashboard (`/admin/members`), profiles of non-family members returned a 404 error. + +**Root Cause**: The `FamilyController@show` method required a `UserRelationship` record between the authenticated user and the member being viewed. Non-family members don't have this relationship, causing `firstOrFail()` to throw a 404. + +**Solution**: Modified `FamilyController@show` method to: +- Check if the authenticated user has the `super-admin` role +- Allow super-admins to view any member's profile without requiring a family relationship +- Create a mock relationship object for admin views to maintain compatibility with the existing view +- Maintain the existing family relationship check for regular users + +### 2. 404 Error for Non-Family Members (Edit Profile) +**Problem**: When accessing `/family/{id}/edit` for non-family members, the page returned a 404 error. + +**Root Cause**: Same as above - the `edit` method required a family relationship. + +**Solution**: Modified `FamilyController@edit` method with the same approach as the `show` method. + +### 3. Update & Delete Permissions +**Problem**: Super-admins couldn't update or delete non-family members. + +**Solution**: +- Modified `FamilyController@update` method to allow super-admins to update any member +- Modified `FamilyController@destroy` method to allow super-admins to delete any member +- Added proper redirects based on user role (admins redirect to admin panel, regular users to family dashboard) +- Added protection to prevent users from deleting their own account + +### 4. Pixelated Profile Pictures +**Problem**: Profile pictures in member cards appeared pixelated and low quality. + +**Solution**: Added CSS image rendering optimizations: +- Added `image-rendering: -webkit-optimize-contrast` for better image quality +- Added `image-rendering: crisp-edges` for sharper rendering +- Added `backface-visibility: hidden` to prevent rendering issues +- Added font smoothing properties for better overall visual quality + +## New Routes Added + +### Admin Member Management Routes (`routes/web.php`) +```php +// All Members Management (Super Admin only) +Route::get('/members/{id}', [PlatformController::class, 'showMember'])->name('platform.members.show'); +Route::get('/members/{id}/edit', [PlatformController::class, 'editMember'])->name('platform.members.edit'); +Route::put('/members/{id}', [PlatformController::class, 'updateMember'])->name('platform.members.update'); +Route::delete('/members/{id}', [PlatformController::class, 'destroyMember'])->name('platform.members.destroy'); +Route::post('/members/{id}/upload-picture', [PlatformController::class, 'uploadMemberPicture'])->name('platform.members.upload-picture'); +Route::post('/members/{id}/health', [PlatformController::class, 'storeMemberHealth'])->name('platform.members.store-health'); +Route::put('/members/{id}/health/{recordId}', [PlatformController::class, 'updateMemberHealth'])->name('platform.members.update-health'); +Route::post('/members/{id}/tournament', [PlatformController::class, 'storeMemberTournament'])->name('platform.members.store-tournament'); +``` + +### Family Routes (Unchanged) +Family routes remain restricted to actual family relationships: +```php +Route::get('/family/{id}', [FamilyController::class, 'show'])->name('family.show'); +Route::get('/family/{id}/edit', [FamilyController::class, 'edit'])->name('family.edit'); +// ... etc +``` + +## Files Modified + +### 1. `routes/web.php` +**Added**: New admin member management routes under `/admin/members/*` prefix + +### 2. `app/Http/Controllers/Admin/PlatformController.php` +**Added Methods**: +- `showMember($id)` - Display member profile +- `editMember($id)` - Show edit form +- `updateMember(Request $request, $id)` - Update member +- `destroyMember($id)` - Delete member +- `uploadMemberPicture(Request $request, $id)` - Upload profile picture +- `storeMemberHealth(Request $request, $id)` - Add health record +- `updateMemberHealth(Request $request, $id, $recordId)` - Update health record +- `storeMemberTournament(Request $request, $id)` - Add tournament record + +All methods create mock relationship objects for view compatibility. + +### 3. `app/Http/Controllers/FamilyController.php` + +**Changes in `show()` method (line 335)**: +```php +// Check if user is super-admin or viewing their own profile +$isSuperAdmin = $user->hasRole('super-admin'); +$isOwnProfile = $user->id == $id; + +// Get the member to display +$member = User::findOrFail($id); + +// For super-admin or own profile, create a mock relationship +if ($isSuperAdmin || $isOwnProfile) { + $relationship = (object)[ + 'dependent' => $member, + 'relationship_type' => $isOwnProfile ? 'self' : 'admin_view', + 'guardian_user_id' => $user->id, + 'dependent_user_id' => $member->id, + ]; +} else { + // Regular user - must have family relationship + $relationship = UserRelationship::where('guardian_user_id', $user->id) + ->where('dependent_user_id', $id) + ->with('dependent') + ->firstOrFail(); +} +``` + +**Changes in `edit()` method (line 470)**: +- Same logic as `show()` method +- Creates mock relationship for super-admins +- Includes `is_billing_contact` field in mock object + +**Changes in `update()` method (line 487)**: +- Made `relationship_type` validation nullable (not required for admin edits) +- Added super-admin and own profile checks +- Only updates relationship record if user is not admin and not editing own profile +- Redirects to admin panel for super-admins, family dashboard for regular users + +**Changes in `destroy()` method (line 911)**: +- Added super-admin check +- Added protection against self-deletion +- Only checks family relationship for non-admin users +- Redirects to admin panel for super-admins, family dashboard for regular users + +**Reverted Changes**: Removed admin access logic from family controller methods since admin now uses separate routes. + +### 4. `resources/views/admin/platform/members.blade.php` +**Changes**: +1. Updated member card links to use `route('admin.platform.members.show')` instead of `route('family.show')` +2. Added CSS image rendering optimizations for better picture quality + +### 5. `resources/views/family/edit.blade.php` +**Changes**: Added conditional routing based on `relationship_type`: +- Upload URL: Uses admin route if `admin_view`, family route otherwise +- Form action: Uses admin route if `admin_view`, family route otherwise +- Cancel button: Redirects to admin panel if `admin_view`, family dashboard otherwise +- Delete form: Uses admin route if `admin_view`, family route otherwise + +### 6. `resources/views/family/show.blade.php` +**Changes**: Updated form actions for health and tournament modals to use admin routes when `relationship_type === 'admin_view'` + +## Route Structure + +### Admin Routes (Super Admin Only) +- **View Profile**: `/admin/members/{id}` → `admin.platform.members.show` +- **Edit Profile**: `/admin/members/{id}/edit` → `admin.platform.members.edit` +- **Update Profile**: `PUT /admin/members/{id}` → `admin.platform.members.update` +- **Delete Member**: `DELETE /admin/members/{id}` → `admin.platform.members.destroy` +- **Upload Picture**: `POST /admin/members/{id}/upload-picture` → `admin.platform.members.upload-picture` +- **Add Health**: `POST /admin/members/{id}/health` → `admin.platform.members.store-health` +- **Update Health**: `PUT /admin/members/{id}/health/{recordId}` → `admin.platform.members.update-health` +- **Add Tournament**: `POST /admin/members/{id}/tournament` → `admin.platform.members.store-tournament` + +### Family Routes (Authenticated Users) +- **View Profile**: `/family/{id}` → `family.show` (requires family relationship) +- **Edit Profile**: `/family/{id}/edit` → `family.edit` (requires family relationship) +- **Update Profile**: `PUT /family/{id}` → `family.update` (requires family relationship) +- **Delete Member**: `DELETE /family/{id}` → `family.destroy` (requires family relationship) +- All other family routes remain unchanged + +## Testing + +### Admin Access Testing +1. **View Any Member**: + - Log in as super-admin + - Navigate to `/admin/members` + - Click any member card + - Should load profile at `/admin/members/{id}` + +2. **Edit Any Member**: + - From member profile, click edit + - Should navigate to `/admin/members/{id}/edit` + - Make changes and save + - Should redirect to `/admin/members` with success message + +3. **Delete Member**: + - From edit page, click "Remove" + - Confirm deletion + - Should redirect to `/admin/members` + - Verify cannot delete own account + +4. **Add Health/Tournament Records**: + - From member profile, use "Add Health Update" or "Add Tournament" + - Submit forms + - Should save successfully and reload page + +### Family Access Testing +1. **View Family Members**: + - Log in as regular user + - Navigate to `/family` + - Click family member card + - Should load profile at `/family/{id}` + +2. **Cannot Access Non-Family**: + - Try to access `/family/{non-family-id}` + - Should return 404 error + +3. **Edit Family Members**: + - From family member profile, click edit + - Should navigate to `/family/{id}/edit` + - Make changes and save + - Should redirect to `/family` dashboard + +### Image Quality Testing +- Check member cards in `/admin/members` +- Profile pictures should appear crisp and clear +- No pixelation on hover or zoom + +## Security Considerations + +- Super-admin role check ensures only authorized users can view/edit/delete all member profiles +- Regular users are still restricted to their family members only +- Self-deletion is prevented for all users +- All existing authorization checks remain in place + +## Backward Compatibility + +- All existing functionality for regular users remains unchanged +- Family relationship checks are still enforced for non-admin users +- The view templates work seamlessly with both real and mock relationship objects +- Redirects are context-aware (admin panel vs family dashboard) diff --git a/app/Http/Controllers/Admin/PlatformController.php b/app/Http/Controllers/Admin/PlatformController.php index ee1d29b..5b3d1fc 100644 --- a/app/Http/Controllers/Admin/PlatformController.php +++ b/app/Http/Controllers/Admin/PlatformController.php @@ -5,9 +5,16 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Models\Tenant; use App\Models\User; +use App\Models\HealthRecord; +use App\Models\Invoice; +use App\Models\TournamentEvent; +use App\Models\Goal; +use App\Models\Attendance; +use App\Models\ClubAffiliation; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Facades\Auth; class PlatformController extends Controller { @@ -477,4 +484,401 @@ class PlatformController extends Controller return response()->json(['success' => false, 'message' => $e->getMessage()], 500); } } + + /** + * Display the specified member's profile. + */ + public function showMember($id) + { + $member = User::findOrFail($id); + + // Fetch health data + $latestHealthRecord = $member->healthRecords()->latest('recorded_at')->first(); + $healthRecords = $member->healthRecords()->orderBy('recorded_at', 'desc')->paginate(10); + $comparisonRecords = $member->healthRecords()->orderBy('recorded_at', 'desc')->take(2)->get(); + + // Fetch invoices + $invoices = Invoice::where('student_user_id', $member->id) + ->orWhere('payer_user_id', $member->id) + ->with(['student', 'tenant']) + ->get(); + + // Fetch tournament data + $tournamentEvents = $member->tournamentEvents() + ->with(['performanceResults', 'notesMedia', 'clubAffiliation']) + ->orderBy('date', 'desc') + ->get(); + + // Calculate award counts + $awardCounts = [ + 'special' => $tournamentEvents->flatMap->performanceResults->where('medal_type', 'special')->count(), + '1st' => $tournamentEvents->flatMap->performanceResults->where('medal_type', '1st')->count(), + '2nd' => $tournamentEvents->flatMap->performanceResults->where('medal_type', '2nd')->count(), + '3rd' => $tournamentEvents->flatMap->performanceResults->where('medal_type', '3rd')->count(), + ]; + + // Get unique sports for filter + $sports = $tournamentEvents->pluck('sport')->unique()->sort()->values(); + + // Fetch goals data + $goals = $member->goals()->orderBy('created_at', 'desc')->get(); + $activeGoalsCount = $goals->where('status', 'active')->count(); + $completedGoalsCount = $goals->where('status', 'completed')->count(); + $successRate = $goals->count() > 0 ? round(($completedGoalsCount / $goals->count()) * 100) : 0; + + // Fetch attendance data + $attendanceRecords = $member->attendanceRecords()->orderBy('session_datetime', 'desc')->get(); + $sessionsCompleted = $attendanceRecords->where('status', 'completed')->count(); + $noShows = $attendanceRecords->where('status', 'no_show')->count(); + $totalSessions = $attendanceRecords->count(); + $attendanceRate = $totalSessions > 0 ? round(($sessionsCompleted / $totalSessions) * 100, 1) : 0; + + // Fetch affiliations data + $clubAffiliations = $member->clubAffiliations() + ->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(); + + // Add icon_class to media items + $clubAffiliations->each(function($affiliation) { + $affiliation->affiliationMedia->each(function($media) { + $media->icon_class = $media->icon_class; + }); + }); + + // Calculate summary stats + $totalAffiliations = $clubAffiliations->count(); + $distinctSkills = $clubAffiliations->flatMap->skillAcquisitions->pluck('skill_name')->unique()->count(); + $totalMembershipDuration = $clubAffiliations->sum('duration_in_months'); + + // Get all unique skills + $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(); + + // Create a mock relationship for the view + $relationship = (object)[ + 'dependent' => $member, + 'relationship_type' => 'admin_view', + 'guardian_user_id' => Auth::id(), + 'dependent_user_id' => $member->id, + ]; + + return view('family.show', [ + 'relationship' => $relationship, + 'latestHealthRecord' => $latestHealthRecord, + 'healthRecords' => $healthRecords, + 'comparisonRecords' => $comparisonRecords, + 'invoices' => $invoices, + 'tournamentEvents' => $tournamentEvents, + 'awardCounts' => $awardCounts, + 'sports' => $sports, + 'goals' => $goals, + 'activeGoalsCount' => $activeGoalsCount, + 'completedGoalsCount' => $completedGoalsCount, + 'successRate' => $successRate, + 'attendanceRecords' => $attendanceRecords, + 'sessionsCompleted' => $sessionsCompleted, + 'noShows' => $noShows, + 'attendanceRate' => $attendanceRate, + 'clubAffiliations' => $clubAffiliations, + 'totalAffiliations' => $totalAffiliations, + 'distinctSkills' => $distinctSkills, + 'totalMembershipDuration' => $totalMembershipDuration, + 'allSkills' => $allSkills, + 'totalInstructors' => $totalInstructors, + 'user' => $member, + ]); + } + + /** + * Show the form for editing a member. + */ + public function editMember($id) + { + $member = User::findOrFail($id); + + // Create a mock relationship for the view + $relationship = (object)[ + 'dependent' => $member, + 'relationship_type' => 'admin_view', + 'guardian_user_id' => Auth::id(), + 'dependent_user_id' => $member->id, + 'is_billing_contact' => false, + ]; + + return view('family.edit', compact('relationship')); + } + + /** + * Update a member. + */ + public function updateMember(Request $request, $id) + { + $validated = $request->validate([ + 'full_name' => 'required|string|max:255', + 'email' => 'nullable|email|max:255|unique:users,email,' . $id, + 'mobile_code' => 'nullable|string|max:5', + 'mobile' => 'nullable|string|max:20', + 'gender' => 'required|in:m,f', + 'marital_status' => 'nullable|in:single,married,divorced,widowed', + 'birthdate' => 'required|date', + 'blood_type' => 'nullable|string|max:10', + 'nationality' => 'required|string|max:100', + 'social_links' => 'nullable|array', + 'social_links.*.platform' => 'required_with:social_links.*.url|string', + 'social_links.*.url' => 'required_with:social_links.*.platform|url', + 'motto' => 'nullable|string|max:500', + ]); + + // Process social links + $socialLinks = []; + if (isset($validated['social_links']) && is_array($validated['social_links'])) { + foreach ($validated['social_links'] as $link) { + if (!empty($link['platform']) && !empty($link['url'])) { + $socialLinks[$link['platform']] = $link['url']; + } + } + } + + // Process mobile + $mobile = [ + 'code' => $validated['mobile_code'] ?? null, + 'number' => $validated['mobile'] ?? null, + ]; + + $member = User::findOrFail($id); + $member->update([ + 'full_name' => $validated['full_name'], + 'email' => $validated['email'], + 'mobile' => $mobile, + 'gender' => $validated['gender'], + 'marital_status' => $validated['marital_status'] ?? null, + 'birthdate' => $validated['birthdate'], + 'blood_type' => $validated['blood_type'], + 'nationality' => $validated['nationality'], + 'social_links' => $socialLinks, + 'motto' => $validated['motto'], + ]); + + // Return JSON for AJAX requests + if ($request->wantsJson() || $request->ajax()) { + return response()->json([ + 'success' => true, + 'message' => 'Member updated successfully.' + ]); + } + + return redirect()->route('admin.platform.members.show', $id) + ->with('success', 'Member updated successfully.'); + } + + /** + * Delete a member. + */ + public function destroyMember($id) + { + // Prevent deleting own account + if (Auth::id() == $id) { + return redirect()->back() + ->with('error', 'You cannot delete your own account.'); + } + + $member = User::findOrFail($id); + $memberName = $member->full_name; + $member->delete(); + + return redirect()->route('admin.platform.members') + ->with('success', $memberName . ' has been removed successfully.'); + } + + /** + * Upload member profile picture. + */ + public function uploadMemberPicture(Request $request, $id) + { + $request->validate([ + 'image' => 'required', + 'folder' => 'required|string', + 'filename' => 'required|string', + ]); + + try { + $member = User::findOrFail($id); + + // 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 profile picture if exists + if ($member->profile_picture && Storage::disk('public')->exists($member->profile_picture)) { + Storage::disk('public')->delete($member->profile_picture); + } + + // Store in the public disk + Storage::disk('public')->put($fullPath, $imageBinary); + + // Update member's profile_picture field + $member->update(['profile_picture' => $fullPath]); + + return response()->json([ + 'success' => true, + 'path' => $fullPath, + 'url' => asset('storage/' . $fullPath) + ]); + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => $e->getMessage()], 500); + } + } + + /** + * Store a health record for a member. + */ + public function storeMemberHealth(Request $request, $id) + { + $validated = $request->validate([ + 'recorded_at' => 'required|date', + 'height' => 'nullable|numeric|min:50|max:250', + 'weight' => 'nullable|numeric|min:0|max:999.9', + 'body_fat_percentage' => 'nullable|numeric|min:0|max:100', + 'bmi' => 'nullable|numeric|min:0|max:100', + 'body_water_percentage' => 'nullable|numeric|min:0|max:100', + 'muscle_mass' => 'nullable|numeric|min:0|max:999.9', + 'bone_mass' => 'nullable|numeric|min:0|max:999.9', + 'visceral_fat' => 'nullable|integer|min:0|max:50', + 'bmr' => 'nullable|integer|min:0|max:10000', + 'protein_percentage' => 'nullable|numeric|min:0|max:100', + 'body_age' => 'nullable|integer|min:0|max:150', + ]); + + $member = User::findOrFail($id); + + // Check for duplicate date + $existing = $member->healthRecords()->where('recorded_at', $validated['recorded_at'])->first(); + if ($existing) { + return redirect()->back() + ->with('error', 'A health record already exists for this date.'); + } + + $member->healthRecords()->create($validated); + + return redirect()->back()->withFragment('health') + ->with('success', 'Health record added successfully.'); + } + + /** + * Update a health record for a member. + */ + public function updateMemberHealth(Request $request, $id, $recordId) + { + $validated = $request->validate([ + 'recorded_at' => 'required|date', + 'height' => 'nullable|numeric|min:50|max:250', + 'weight' => 'nullable|numeric|min:0|max:999.9', + 'body_fat_percentage' => 'nullable|numeric|min:0|max:100', + 'bmi' => 'nullable|numeric|min:0|max:100', + 'body_water_percentage' => 'nullable|numeric|min:0|max:100', + 'muscle_mass' => 'nullable|numeric|min:0|max:999.9', + 'bone_mass' => 'nullable|numeric|min:0|max:999.9', + 'visceral_fat' => 'nullable|integer|min:0|max:50', + 'bmr' => 'nullable|integer|min:0|max:10000', + 'protein_percentage' => 'nullable|numeric|min:0|max:100', + 'body_age' => 'nullable|integer|min:0|max:150', + ]); + + $member = User::findOrFail($id); + $healthRecord = $member->healthRecords()->findOrFail($recordId); + + // Check for duplicate date (excluding current record) + $existing = $member->healthRecords() + ->where('recorded_at', $validated['recorded_at']) + ->where('id', '!=', $recordId) + ->first(); + if ($existing) { + return redirect()->back() + ->with('error', 'A health record already exists for this date.'); + } + + $healthRecord->update($validated); + + return redirect()->back()->withFragment('health') + ->with('success', 'Health record updated successfully.'); + } + + /** + * Store a tournament record for a member. + */ + public function storeMemberTournament(Request $request, $id) + { + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'type' => 'required|in:championship,tournament,competition,exhibition', + 'sport' => 'required|string|max:100', + 'date' => 'required|date', + 'time' => 'nullable|date_format:H:i', + 'location' => 'nullable|string|max:255', + 'participants_count' => 'nullable|integer|min:1', + 'club_affiliation_id' => 'nullable|exists:club_affiliations,id', + 'performance_results' => 'nullable|array', + 'performance_results.*.medal_type' => 'nullable|in:special,1st,2nd,3rd', + 'performance_results.*.points' => 'nullable|numeric|min:0', + 'performance_results.*.description' => 'nullable|string|max:500', + 'notes_media' => 'nullable|array', + 'notes_media.*.note_text' => 'nullable|string|max:1000', + 'notes_media.*.media_link' => 'nullable|url', + ]); + + // Create the tournament event + $tournament = TournamentEvent::create([ + 'user_id' => $id, + 'club_affiliation_id' => $validated['club_affiliation_id'] ?? null, + 'title' => $validated['title'], + 'type' => $validated['type'], + 'sport' => $validated['sport'], + 'date' => $validated['date'], + 'time' => $validated['time'], + 'location' => $validated['location'], + 'participants_count' => $validated['participants_count'], + ]); + + // Create performance results + if (isset($validated['performance_results'])) { + foreach ($validated['performance_results'] as $resultData) { + if (!empty($resultData['medal_type'])) { + $tournament->performanceResults()->create($resultData); + } + } + } + + // Create notes and media + if (isset($validated['notes_media'])) { + foreach ($validated['notes_media'] as $noteData) { + if (!empty($noteData['note_text']) || !empty($noteData['media_link'])) { + $tournament->notesMedia()->create($noteData); + } + } + } + + return response()->json(['success' => true, 'message' => 'Tournament record added successfully']); + } } diff --git a/app/Http/Controllers/FamilyController.php b/app/Http/Controllers/FamilyController.php index 334875b..320ad57 100644 --- a/app/Http/Controllers/FamilyController.php +++ b/app/Http/Controllers/FamilyController.php @@ -254,6 +254,7 @@ class FamilyController extends Controller 'mobile_code' => 'nullable|string|max:5', 'mobile' => 'nullable|string|max:20', 'gender' => 'required|in:m,f', + 'marital_status' => 'nullable|in:single,married,divorced,widowed', 'birthdate' => 'required|date', 'blood_type' => 'nullable|string|max:10', 'nationality' => 'required|string|max:100', @@ -261,10 +262,32 @@ class FamilyController extends Controller 'social_links.*.platform' => 'required_with:social_links.*.url|string', 'social_links.*.url' => 'required_with:social_links.*.platform|url', 'motto' => 'nullable|string|max:500', + 'remove_profile_picture' => 'nullable|boolean', + 'profile_picture_is_public' => 'nullable|boolean', ]); $user = Auth::user(); + // Handle profile picture removal + if ($request->input('remove_profile_picture') == '1') { + // Delete the profile picture file if it exists + if ($user->profile_picture && Storage::disk('public')->exists($user->profile_picture)) { + Storage::disk('public')->delete($user->profile_picture); + } + + // Also check for old format profile pictures + $extensions = ['png', 'jpg', 'jpeg', 'webp']; + foreach ($extensions as $ext) { + $path = 'images/profiles/profile_' . $user->id . '.' . $ext; + if (Storage::disk('public')->exists($path)) { + Storage::disk('public')->delete($path); + } + } + + // Set profile_picture to null + $user->profile_picture = null; + } + // Process social links - convert from array of objects to associative array $socialLinks = []; if (isset($validated['social_links']) && is_array($validated['social_links'])) { @@ -284,6 +307,9 @@ class FamilyController extends Controller ]; unset($validated['mobile_code']); + // Handle profile picture visibility + $validated['profile_picture_is_public'] = $request->has('profile_picture_is_public') ? true : false; + $user->update($validated); return redirect()->route('profile.show') @@ -335,10 +361,30 @@ class FamilyController extends Controller public function show($id) { $user = Auth::user(); - $relationship = UserRelationship::where('guardian_user_id', $user->id) - ->where('dependent_user_id', $id) - ->with('dependent') - ->firstOrFail(); + + // Check if user is super-admin or viewing their own profile + $isSuperAdmin = $user->hasRole('super-admin'); + $isOwnProfile = $user->id == $id; + + // Get the member to display + $member = User::findOrFail($id); + + // For super-admin or own profile, create a mock relationship + // For regular users, verify family relationship exists + if ($isSuperAdmin || $isOwnProfile) { + $relationship = (object)[ + 'dependent' => $member, + 'relationship_type' => $isOwnProfile ? 'self' : 'admin_view', + 'guardian_user_id' => $user->id, + 'dependent_user_id' => $member->id, + ]; + } else { + // Regular user - must have family relationship + $relationship = UserRelationship::where('guardian_user_id', $user->id) + ->where('dependent_user_id', $id) + ->with('dependent') + ->firstOrFail(); + } // Fetch health data for the dependent $latestHealthRecord = $relationship->dependent->healthRecords()->latest('recorded_at')->first(); @@ -450,10 +496,31 @@ class FamilyController extends Controller public function edit($id) { $user = Auth::user(); - $relationship = UserRelationship::where('guardian_user_id', $user->id) - ->where('dependent_user_id', $id) - ->with('dependent') - ->firstOrFail(); + + // Check if user is super-admin or viewing their own profile + $isSuperAdmin = $user->hasRole('super-admin'); + $isOwnProfile = $user->id == $id; + + // Get the member to edit + $member = User::findOrFail($id); + + // For super-admin or own profile, create a mock relationship + // For regular users, verify family relationship exists + if ($isSuperAdmin || $isOwnProfile) { + $relationship = (object)[ + 'dependent' => $member, + 'relationship_type' => $isOwnProfile ? 'self' : 'admin_view', + 'guardian_user_id' => $user->id, + 'dependent_user_id' => $member->id, + 'is_billing_contact' => false, + ]; + } else { + // Regular user - must have family relationship + $relationship = UserRelationship::where('guardian_user_id', $user->id) + ->where('dependent_user_id', $id) + ->with('dependent') + ->firstOrFail(); + } return view('family.edit', compact('relationship')); } @@ -473,6 +540,7 @@ class FamilyController extends Controller 'mobile_code' => 'nullable|string|max:5', 'mobile' => 'nullable|string|max:20', 'gender' => 'required|in:m,f', + 'marital_status' => 'nullable|in:single,married,divorced,widowed', 'birthdate' => 'required|date', 'blood_type' => 'nullable|string|max:10', 'nationality' => 'required|string|max:100', @@ -480,14 +548,46 @@ class FamilyController extends Controller 'social_links.*.platform' => 'required_with:social_links.*.url|string', 'social_links.*.url' => 'required_with:social_links.*.platform|url', 'motto' => 'nullable|string|max:500', - 'relationship_type' => 'required|string|max:50', + 'relationship_type' => 'nullable|string|max:50', 'is_billing_contact' => 'boolean', + 'remove_profile_picture' => 'nullable|boolean', + 'profile_picture_is_public' => 'nullable|boolean', ]); $user = Auth::user(); - $relationship = UserRelationship::where('guardian_user_id', $user->id) - ->where('dependent_user_id', $id) - ->firstOrFail(); + + // Check if user is super-admin or updating their own profile + $isSuperAdmin = $user->hasRole('super-admin'); + $isOwnProfile = $user->id == $id; + + // For regular users, verify family relationship exists + if (!$isSuperAdmin && !$isOwnProfile) { + $relationship = UserRelationship::where('guardian_user_id', $user->id) + ->where('dependent_user_id', $id) + ->firstOrFail(); + } + + $dependent = User::findOrFail($id); + + // Handle profile picture removal + if ($request->input('remove_profile_picture') == '1') { + // Delete the profile picture file if it exists + if ($dependent->profile_picture && Storage::disk('public')->exists($dependent->profile_picture)) { + Storage::disk('public')->delete($dependent->profile_picture); + } + + // Also check for old format profile pictures + $extensions = ['png', 'jpg', 'jpeg', 'webp']; + foreach ($extensions as $ext) { + $path = 'images/profiles/profile_' . $dependent->id . '.' . $ext; + if (Storage::disk('public')->exists($path)) { + Storage::disk('public')->delete($path); + } + } + + // Set profile_picture to null + $dependent->profile_picture = null; + } // Process social links - convert from array of objects to associative array $socialLinks = []; @@ -505,23 +605,33 @@ class FamilyController extends Controller 'number' => $validated['mobile'] ?? null, ]; - $dependent = User::findOrFail($id); $dependent->update([ 'full_name' => $validated['full_name'], 'email' => $validated['email'], 'mobile' => $mobile, 'gender' => $validated['gender'], + 'marital_status' => $validated['marital_status'] ?? null, 'birthdate' => $validated['birthdate'], 'blood_type' => $validated['blood_type'], 'nationality' => $validated['nationality'], 'social_links' => $socialLinks, 'motto' => $validated['motto'], + 'profile_picture_is_public' => $request->has('profile_picture_is_public') ? true : false, ]); - $relationship->update([ - 'relationship_type' => $validated['relationship_type'], - 'is_billing_contact' => $validated['is_billing_contact'] ?? false, - ]); + // Update relationship if it exists (not for admin or own profile) + if (!$isSuperAdmin && !$isOwnProfile && isset($relationship)) { + $relationship->update([ + 'relationship_type' => $validated['relationship_type'] ?? $relationship->relationship_type, + 'is_billing_contact' => $validated['is_billing_contact'] ?? false, + ]); + } + + // Redirect based on user type + if ($isSuperAdmin) { + return redirect()->route('admin.platform.members') + ->with('success', 'Member updated successfully.'); + } return redirect()->route('family.dashboard') ->with('success', 'Family member updated successfully.'); @@ -853,13 +963,33 @@ class FamilyController extends Controller public function destroy($id) { $user = Auth::user(); - $relationship = UserRelationship::where('guardian_user_id', $user->id) - ->where('dependent_user_id', $id) - ->firstOrFail(); + + // Check if user is super-admin + $isSuperAdmin = $user->hasRole('super-admin'); + + // Prevent deleting own account + if ($user->id == $id) { + return redirect()->back() + ->with('error', 'You cannot delete your own account.'); + } + + // For regular users, verify family relationship exists + if (!$isSuperAdmin) { + $relationship = UserRelationship::where('guardian_user_id', $user->id) + ->where('dependent_user_id', $id) + ->firstOrFail(); + } $dependent = User::findOrFail($id); + $memberName = $dependent->full_name; $dependent->delete(); + // Redirect based on user type + if ($isSuperAdmin) { + return redirect()->route('admin.platform.members') + ->with('success', $memberName . ' has been removed successfully.'); + } + return redirect()->route('family.dashboard') ->with('success', 'Family member removed successfully.'); } diff --git a/app/Http/Controllers/MemberController.php b/app/Http/Controllers/MemberController.php new file mode 100644 index 0000000..1bdeb84 --- /dev/null +++ b/app/Http/Controllers/MemberController.php @@ -0,0 +1,709 @@ +id) + ->with('dependent') + ->whereHas('dependent') + ->get() + ->sortBy(function($relationship) { + return $relationship->dependent->full_name; + }); + + return view('member.index', compact('user', 'dependents')); + } + + /** + * Show the form for creating a new member. + * + * @return \Illuminate\View\View + */ + public function create() + { + return view('member.create'); + } + + /** + * Store a newly created member in storage. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function store(Request $request) + { + $validated = $request->validate([ + 'full_name' => 'required|string|max:255', + 'email' => 'nullable|email|max:255', + 'gender' => 'required|in:m,f', + 'birthdate' => 'required|date', + 'blood_type' => 'nullable|string|max:10', + 'nationality' => 'required|string|max:100', + 'relationship_type' => 'required|string|max:50', + 'is_billing_contact' => 'boolean', + ]); + + $guardian = Auth::user(); + + // Use FamilyService to create the dependent + $familyService = app(\App\Services\FamilyService::class); + $dependent = $familyService->createDependent($guardian, $validated); + + return redirect()->route('members.index') + ->with('success', 'Member added successfully.'); + } + + /** + * Display the specified member's profile. + * + * @param int $id + * @return \Illuminate\View\View + */ + public function show($id) + { + $user = Auth::user(); + + // Check if user is super-admin or viewing their own profile + $isSuperAdmin = $user->hasRole('super-admin'); + $isOwnProfile = $user->id == $id; + + // Get the member to display + $member = User::findOrFail($id); + + // For super-admin or own profile, create a mock relationship + // For regular users, verify family relationship exists + if ($isSuperAdmin || $isOwnProfile) { + $relationship = (object)[ + 'dependent' => $member, + 'relationship_type' => $isOwnProfile ? 'self' : 'admin_view', + 'guardian_user_id' => $user->id, + 'dependent_user_id' => $member->id, + ]; + } else { + // Regular user - must have family relationship + $relationship = UserRelationship::where('guardian_user_id', $user->id) + ->where('dependent_user_id', $id) + ->with('dependent') + ->firstOrFail(); + } + + // Fetch health data for the member + $latestHealthRecord = $relationship->dependent->healthRecords()->latest('recorded_at')->first(); + $healthRecords = $relationship->dependent->healthRecords()->orderBy('recorded_at', 'desc')->paginate(10); + $comparisonRecords = $relationship->dependent->healthRecords()->orderBy('recorded_at', 'desc')->take(2)->get(); + + // Fetch invoices for the member + $invoices = Invoice::where('student_user_id', $relationship->dependent->id) + ->orWhere('payer_user_id', $relationship->dependent->id) + ->with(['student', 'tenant']) + ->get(); + + // Fetch tournament data for the member + $tournamentEvents = $relationship->dependent->tournamentEvents() + ->with(['performanceResults', 'notesMedia', 'clubAffiliation']) + ->orderBy('date', 'desc') + ->get(); + + // Calculate award counts + $awardCounts = [ + 'special' => $tournamentEvents->flatMap->performanceResults->where('medal_type', 'special')->count(), + '1st' => $tournamentEvents->flatMap->performanceResults->where('medal_type', '1st')->count(), + '2nd' => $tournamentEvents->flatMap->performanceResults->where('medal_type', '2nd')->count(), + '3rd' => $tournamentEvents->flatMap->performanceResults->where('medal_type', '3rd')->count(), + ]; + + // Get unique sports for filter + $sports = $tournamentEvents->pluck('sport')->unique()->sort()->values(); + + // Fetch goals data for the member + $goals = $relationship->dependent->goals()->orderBy('created_at', 'desc')->get(); + $activeGoalsCount = $goals->where('status', 'active')->count(); + $completedGoalsCount = $goals->where('status', 'completed')->count(); + $successRate = $goals->count() > 0 ? round(($completedGoalsCount / $goals->count()) * 100) : 0; + + // Fetch attendance data for the member + $attendanceRecords = $relationship->dependent->attendanceRecords()->orderBy('session_datetime', 'desc')->get(); + $sessionsCompleted = $attendanceRecords->where('status', 'completed')->count(); + $noShows = $attendanceRecords->where('status', 'no_show')->count(); + $totalSessions = $attendanceRecords->count(); + $attendanceRate = $totalSessions > 0 ? round(($sessionsCompleted / $totalSessions) * 100, 1) : 0; + + // Fetch affiliations data for the member with enhanced relationships + $clubAffiliations = $relationship->dependent->clubAffiliations() + ->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(); + + // Add icon_class to media items for JavaScript + $clubAffiliations->each(function($affiliation) { + $affiliation->affiliationMedia->each(function($media) { + $media->icon_class = $media->icon_class; + }); + }); + + // Calculate summary stats + $totalAffiliations = $clubAffiliations->count(); + $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('member.show', [ + 'relationship' => $relationship, + 'latestHealthRecord' => $latestHealthRecord, + 'healthRecords' => $healthRecords, + 'comparisonRecords' => $comparisonRecords, + 'invoices' => $invoices, + 'tournamentEvents' => $tournamentEvents, + 'awardCounts' => $awardCounts, + 'sports' => $sports, + 'goals' => $goals, + 'activeGoalsCount' => $activeGoalsCount, + 'completedGoalsCount' => $completedGoalsCount, + 'successRate' => $successRate, + 'attendanceRecords' => $attendanceRecords, + 'sessionsCompleted' => $sessionsCompleted, + 'noShows' => $noShows, + 'attendanceRate' => $attendanceRate, + 'clubAffiliations' => $clubAffiliations, + 'totalAffiliations' => $totalAffiliations, + 'distinctSkills' => $distinctSkills, + 'totalMembershipDuration' => $totalMembershipDuration, + 'allSkills' => $allSkills, + 'totalInstructors' => $totalInstructors, + 'user' => $relationship->dependent, + ]); + } + + /** + * Show the form for editing the specified member. + * + * @param int $id + * @return \Illuminate\View\View + */ + public function edit($id) + { + $user = Auth::user(); + + // Check if user is super-admin or viewing their own profile + $isSuperAdmin = $user->hasRole('super-admin'); + $isOwnProfile = $user->id == $id; + + // Get the member to edit + $member = User::findOrFail($id); + + // For super-admin or own profile, create a mock relationship + // For regular users, verify family relationship exists + if ($isSuperAdmin || $isOwnProfile) { + $relationship = (object)[ + 'dependent' => $member, + 'relationship_type' => $isOwnProfile ? 'self' : 'admin_view', + 'guardian_user_id' => $user->id, + 'dependent_user_id' => $member->id, + 'is_billing_contact' => false, + ]; + } else { + // Regular user - must have family relationship + $relationship = UserRelationship::where('guardian_user_id', $user->id) + ->where('dependent_user_id', $id) + ->with('dependent') + ->firstOrFail(); + } + + return view('member.edit', compact('relationship')); + } + + /** + * Update the specified member in storage. + * + * @param \Illuminate\Http\Request $request + * @param int $id + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $id) + { + $validated = $request->validate([ + 'full_name' => 'required|string|max:255', + 'email' => 'nullable|email|max:255|unique:users,email,' . $id, + 'mobile_code' => 'nullable|string|max:5', + 'mobile' => 'nullable|string|max:20', + 'gender' => 'required|in:m,f', + 'marital_status' => 'nullable|in:single,married,divorced,widowed', + 'birthdate' => 'required|date', + 'blood_type' => 'nullable|string|max:10', + 'nationality' => 'required|string|max:100', + 'social_links' => 'nullable|array', + 'social_links.*.platform' => 'required_with:social_links.*.url|string', + 'social_links.*.url' => 'required_with:social_links.*.platform|url', + 'motto' => 'nullable|string|max:500', + 'relationship_type' => 'nullable|string|max:50', + 'is_billing_contact' => 'boolean', + 'remove_profile_picture' => 'nullable|boolean', + 'profile_picture_is_public' => 'nullable|boolean', + ]); + + $user = Auth::user(); + + // Check if user is super-admin or updating their own profile + $isSuperAdmin = $user->hasRole('super-admin'); + $isOwnProfile = $user->id == $id; + + // For regular users, verify family relationship exists + if (!$isSuperAdmin && !$isOwnProfile) { + $relationship = UserRelationship::where('guardian_user_id', $user->id) + ->where('dependent_user_id', $id) + ->firstOrFail(); + } + + // Process social links - convert from array of objects to associative array + $socialLinks = []; + if (isset($validated['social_links']) && is_array($validated['social_links'])) { + foreach ($validated['social_links'] as $link) { + if (!empty($link['platform']) && !empty($link['url'])) { + $socialLinks[$link['platform']] = $link['url']; + } + } + } + + // Process mobile + $mobile = [ + 'code' => $validated['mobile_code'] ?? null, + 'number' => $validated['mobile'] ?? null, + ]; + + $member = User::findOrFail($id); + + // Handle profile picture removal + if ($request->input('remove_profile_picture') == '1') { + // Delete the profile picture file if it exists + if ($member->profile_picture && Storage::disk('public')->exists($member->profile_picture)) { + Storage::disk('public')->delete($member->profile_picture); + } + + // Also check for old format profile pictures + $extensions = ['png', 'jpg', 'jpeg', 'webp']; + foreach ($extensions as $ext) { + $path = 'images/profiles/profile_' . $member->id . '.' . $ext; + if (Storage::disk('public')->exists($path)) { + Storage::disk('public')->delete($path); + } + } + + // Set profile_picture to null + $member->profile_picture = null; + } + + $member->update([ + 'full_name' => $validated['full_name'], + 'email' => $validated['email'], + 'mobile' => $mobile, + 'gender' => $validated['gender'], + 'marital_status' => $validated['marital_status'] ?? null, + 'birthdate' => $validated['birthdate'], + 'blood_type' => $validated['blood_type'], + 'nationality' => $validated['nationality'], + 'social_links' => $socialLinks, + 'motto' => $validated['motto'], + 'profile_picture_is_public' => $request->has('profile_picture_is_public') ? true : false, + ]); + + // Update relationship if it exists (not for admin or own profile) + if (!$isSuperAdmin && !$isOwnProfile && isset($relationship)) { + $relationship->update([ + 'relationship_type' => $validated['relationship_type'] ?? $relationship->relationship_type, + 'is_billing_contact' => $validated['is_billing_contact'] ?? false, + ]); + } + + // Return JSON for AJAX requests + if ($request->wantsJson() || $request->ajax()) { + // Get the updated profile picture URL + $profilePictureUrl = null; + if ($member->profile_picture && file_exists(public_path('storage/' . $member->profile_picture))) { + $profilePictureUrl = asset('storage/' . $member->profile_picture); + } else { + $extensions = ['png', 'jpg', 'jpeg', 'webp']; + foreach ($extensions as $ext) { + $path = 'storage/images/profiles/profile_' . $member->id . '.' . $ext; + if (file_exists(public_path($path))) { + $profilePictureUrl = asset($path); + break; + } + } + } + + return response()->json([ + 'success' => true, + 'message' => 'Member updated successfully.', + 'profile_picture_url' => $profilePictureUrl + ]); + } + + return redirect()->route('member.show', $id) + ->with('success', 'Member updated successfully.'); + } + + /** + * Upload profile picture for a member. + * + * @param \Illuminate\Http\Request $request + * @param int $id + * @return \Illuminate\Http\JsonResponse + */ + public function uploadPicture(Request $request, $id) + { + $request->validate([ + 'image' => 'required', + 'folder' => 'required|string', + 'filename' => 'required|string', + ]); + + try { + $user = Auth::user(); + + // Check if user is super-admin or uploading their own picture + $isSuperAdmin = $user->hasRole('super-admin'); + $isOwnProfile = $user->id == $id; + + // For regular users, verify family relationship exists + if (!$isSuperAdmin && !$isOwnProfile) { + UserRelationship::where('guardian_user_id', $user->id) + ->where('dependent_user_id', $id) + ->firstOrFail(); + } + + $member = User::findOrFail($id); + + // 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 profile picture if exists + if ($member->profile_picture && Storage::disk('public')->exists($member->profile_picture)) { + Storage::disk('public')->delete($member->profile_picture); + } + + // Store in the public disk + Storage::disk('public')->put($fullPath, $imageBinary); + + // Update member's profile_picture field + $member->update(['profile_picture' => $fullPath]); + + return response()->json([ + 'success' => true, + 'path' => $fullPath, + 'url' => asset('storage/' . $fullPath) + ]); + } catch (\Exception $e) { + return response()->json(['success' => false, 'message' => $e->getMessage()], 500); + } + } + + /** + * Store a health record for the specified member. + * + * @param \Illuminate\Http\Request $request + * @param int $id + * @return \Illuminate\Http\RedirectResponse + */ + public function storeHealth(Request $request, $id) + { + $validated = $request->validate([ + 'recorded_at' => 'required|date', + 'height' => 'nullable|numeric|min:50|max:250', + 'weight' => 'nullable|numeric|min:0|max:999.9', + 'body_fat_percentage' => 'nullable|numeric|min:0|max:100', + 'bmi' => 'nullable|numeric|min:0|max:100', + 'body_water_percentage' => 'nullable|numeric|min:0|max:100', + 'muscle_mass' => 'nullable|numeric|min:0|max:999.9', + 'bone_mass' => 'nullable|numeric|min:0|max:999.9', + 'visceral_fat' => 'nullable|integer|min:0|max:50', + 'bmr' => 'nullable|integer|min:0|max:10000', + 'protein_percentage' => 'nullable|numeric|min:0|max:100', + 'body_age' => 'nullable|integer|min:0|max:150', + ]); + + $user = Auth::user(); + + // Check if user is super-admin or adding health for themselves + $isSuperAdmin = $user->hasRole('super-admin'); + $isOwnProfile = $user->id == $id; + + // For regular users, verify family relationship exists + if (!$isSuperAdmin && !$isOwnProfile) { + UserRelationship::where('guardian_user_id', $user->id) + ->where('dependent_user_id', $id) + ->firstOrFail(); + } + + $member = User::findOrFail($id); + + // Check for duplicate date + $existing = $member->healthRecords()->where('recorded_at', $validated['recorded_at'])->first(); + if ($existing) { + return redirect()->back() + ->with('error', 'A health record already exists for this date.'); + } + + $member->healthRecords()->create($validated); + + return redirect()->back()->withFragment('health') + ->with('success', 'Health record added successfully.'); + } + + /** + * Update a health record for the specified member. + * + * @param \Illuminate\Http\Request $request + * @param int $id + * @param int $recordId + * @return \Illuminate\Http\RedirectResponse + */ + public function updateHealth(Request $request, $id, $recordId) + { + $validated = $request->validate([ + 'recorded_at' => 'required|date', + 'height' => 'nullable|numeric|min:50|max:250', + 'weight' => 'nullable|numeric|min:0|max:999.9', + 'body_fat_percentage' => 'nullable|numeric|min:0|max:100', + 'bmi' => 'nullable|numeric|min:0|max:100', + 'body_water_percentage' => 'nullable|numeric|min:0|max:100', + 'muscle_mass' => 'nullable|numeric|min:0|max:999.9', + 'bone_mass' => 'nullable|numeric|min:0|max:999.9', + 'visceral_fat' => 'nullable|integer|min:0|max:50', + 'bmr' => 'nullable|integer|min:0|max:10000', + 'protein_percentage' => 'nullable|numeric|min:0|max:100', + 'body_age' => 'nullable|integer|min:0|max:150', + ]); + + $user = Auth::user(); + + // Check if user is super-admin or updating their own health + $isSuperAdmin = $user->hasRole('super-admin'); + $isOwnProfile = $user->id == $id; + + // For regular users, verify family relationship exists + if (!$isSuperAdmin && !$isOwnProfile) { + UserRelationship::where('guardian_user_id', $user->id) + ->where('dependent_user_id', $id) + ->firstOrFail(); + } + + $member = User::findOrFail($id); + $healthRecord = $member->healthRecords()->findOrFail($recordId); + + // Check for duplicate date (excluding current record) + $existing = $member->healthRecords() + ->where('recorded_at', $validated['recorded_at']) + ->where('id', '!=', $recordId) + ->first(); + if ($existing) { + return redirect()->back() + ->with('error', 'A health record already exists for this date.'); + } + + $healthRecord->update($validated); + + return redirect()->back()->withFragment('health') + ->with('success', 'Health record updated successfully.'); + } + + /** + * Store a tournament record for the specified member. + * + * @param \Illuminate\Http\Request $request + * @param int $id + * @return \Illuminate\Http\JsonResponse + */ + public function storeTournament(Request $request, $id) + { + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'type' => 'required|in:championship,tournament,competition,exhibition', + 'sport' => 'required|string|max:100', + 'date' => 'required|date', + 'time' => 'nullable|date_format:H:i', + 'location' => 'nullable|string|max:255', + 'participants_count' => 'nullable|integer|min:1', + 'club_affiliation_id' => 'nullable|exists:club_affiliations,id', + 'performance_results' => 'nullable|array', + 'performance_results.*.medal_type' => 'nullable|in:special,1st,2nd,3rd', + 'performance_results.*.points' => 'nullable|numeric|min:0', + 'performance_results.*.description' => 'nullable|string|max:500', + 'notes_media' => 'nullable|array', + 'notes_media.*.note_text' => 'nullable|string|max:1000', + 'notes_media.*.media_link' => 'nullable|url', + ]); + + $user = Auth::user(); + + // Check if user is super-admin or adding tournament for themselves + $isSuperAdmin = $user->hasRole('super-admin'); + $isOwnProfile = $user->id == $id; + + // For regular users, verify family relationship exists + if (!$isSuperAdmin && !$isOwnProfile) { + UserRelationship::where('guardian_user_id', $user->id) + ->where('dependent_user_id', $id) + ->firstOrFail(); + } + + // Create the tournament event + $tournament = TournamentEvent::create([ + 'user_id' => $id, + 'club_affiliation_id' => $validated['club_affiliation_id'] ?? null, + 'title' => $validated['title'], + 'type' => $validated['type'], + 'sport' => $validated['sport'], + 'date' => $validated['date'], + 'time' => $validated['time'], + 'location' => $validated['location'], + 'participants_count' => $validated['participants_count'], + ]); + + // Create performance results + if (isset($validated['performance_results'])) { + foreach ($validated['performance_results'] as $resultData) { + if (!empty($resultData['medal_type'])) { + $tournament->performanceResults()->create($resultData); + } + } + } + + // Create notes and media + if (isset($validated['notes_media'])) { + foreach ($validated['notes_media'] as $noteData) { + if (!empty($noteData['note_text']) || !empty($noteData['media_link'])) { + $tournament->notesMedia()->create($noteData); + } + } + } + + return response()->json(['success' => true, 'message' => 'Tournament record added successfully']); + } + + /** + * Update the specified goal. + * + * @param \Illuminate\Http\Request $request + * @param int $goalId + * @return \Illuminate\Http\JsonResponse + */ + public function updateGoal(Request $request, $goalId) + { + $user = Auth::user(); + + // Find the goal + $goal = Goal::findOrFail($goalId); + + // Check if user is super-admin + $isSuperAdmin = $user->hasRole('super-admin'); + + // Check if user is authorized to update this goal + if (!$isSuperAdmin && $goal->user_id !== $user->id) { + // Check if user is guardian of the goal owner + $relationship = UserRelationship::where('guardian_user_id', $user->id) + ->where('dependent_user_id', $goal->user_id) + ->first(); + + if (!$relationship) { + return response()->json(['success' => false, 'message' => 'Unauthorized'], 403); + } + } + + // Validate the request + $validated = $request->validate([ + 'current_progress_value' => 'required|numeric|min:0', + 'status' => 'required|in:active,completed', + ]); + + // Update the goal + $goal->update($validated); + + return response()->json(['success' => true, 'message' => 'Goal updated successfully']); + } + + /** + * Remove the specified member from storage. + * + * @param int $id + * @return \Illuminate\Http\RedirectResponse + */ + public function destroy($id) + { + $user = Auth::user(); + + // Check if user is super-admin + $isSuperAdmin = $user->hasRole('super-admin'); + + // Prevent deleting own account + if ($user->id == $id) { + return redirect()->back() + ->with('error', 'You cannot delete your own account.'); + } + + // For regular users, verify family relationship exists + if (!$isSuperAdmin) { + UserRelationship::where('guardian_user_id', $user->id) + ->where('dependent_user_id', $id) + ->firstOrFail(); + } + + $member = User::findOrFail($id); + $memberName = $member->full_name; + $member->delete(); + + // Redirect based on user type + if ($isSuperAdmin) { + return redirect()->route('admin.platform.members') + ->with('success', $memberName . ' has been removed successfully.'); + } + + return redirect()->route('members.index') + ->with('success', 'Member removed successfully.'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index a4d9c8f..d8a3432 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -29,6 +29,7 @@ class User extends Authenticatable 'mobile', 'password', 'gender', + 'marital_status', 'birthdate', 'blood_type', 'nationality', @@ -36,6 +37,7 @@ class User extends Authenticatable 'social_links', 'media_gallery', 'profile_picture', + 'profile_picture_is_public', 'motto', ]; diff --git a/database/migrations/2026_01_27_185405_add_marital_status_to_users_table.php b/database/migrations/2026_01_27_185405_add_marital_status_to_users_table.php new file mode 100644 index 0000000..eb83cb2 --- /dev/null +++ b/database/migrations/2026_01_27_185405_add_marital_status_to_users_table.php @@ -0,0 +1,28 @@ +string('marital_status')->nullable()->after('gender'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('marital_status'); + }); + } +}; diff --git a/database/migrations/2026_01_27_221321_add_profile_picture_visibility_to_users_table.php b/database/migrations/2026_01_27_221321_add_profile_picture_visibility_to_users_table.php new file mode 100644 index 0000000..fb2ac73 --- /dev/null +++ b/database/migrations/2026_01_27_221321_add_profile_picture_visibility_to_users_table.php @@ -0,0 +1,28 @@ +boolean('profile_picture_is_public')->default(true)->after('profile_picture'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('profile_picture_is_public'); + }); + } +}; diff --git a/resources/views/admin/platform/members.blade.php b/resources/views/admin/platform/members.blade.php index 59060fa..cd49ceb 100644 --- a/resources/views/admin/platform/members.blade.php +++ b/resources/views/admin/platform/members.blade.php @@ -32,7 +32,7 @@ data-member-phone="{{ $member->formatted_mobile ?? '' }}" data-member-nationality="{{ $member->nationality ?? '' }}" data-member-gender="{{ $member->gender ?? '' }}"> - +
@@ -40,7 +40,7 @@
@if($member->profile_picture) - {{ $member->full_name }} + {{ $member->full_name }} @else
{{ strtoupper(substr($member->full_name, 0, 1)) }} @@ -224,6 +224,16 @@ .member-card-wrapper.hidden { display: none; } + + /* Improve image quality */ + .rounded-circle img { + image-rendering: -webkit-optimize-contrast; + image-rendering: crisp-edges; + backface-visibility: hidden; + -webkit-backface-visibility: hidden; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } @endpush diff --git a/resources/views/components/edit-profile-modal.blade.php b/resources/views/components/edit-profile-modal.blade.php new file mode 100644 index 0000000..a129722 --- /dev/null +++ b/resources/views/components/edit-profile-modal.blade.php @@ -0,0 +1,1005 @@ +@props([ + 'user', + 'formAction', + 'formMethod' => 'PUT', + 'cancelUrl' => null, + 'showRelationshipFields' => false, + 'relationship' => null, +]) + + + + +@push('styles') + +@endpush + +@push('scripts') + +@endpush diff --git a/resources/views/components/nationality-dropdown.blade.php b/resources/views/components/nationality-dropdown.blade.php index 3d26a8c..3124fc9 100644 --- a/resources/views/components/nationality-dropdown.blade.php +++ b/resources/views/components/nationality-dropdown.blade.php @@ -108,7 +108,7 @@ ${country.name} `; button.addEventListener('click', function() { - selectNationality(componentId, country.name, flagEmoji); + selectNationality(componentId, country.iso3, flagEmoji, country.name); }); countryList.appendChild(button); }); @@ -133,26 +133,31 @@ // Set initial value if provided const hiddenInput = document.getElementById(componentId); if (hiddenInput && hiddenInput.value) { - const initialCountry = countries.find(c => c.name === hiddenInput.value); + // Try to find by ISO3 code first, then by name + let initialCountry = countries.find(c => c.iso3 === hiddenInput.value); + if (!initialCountry) { + initialCountry = countries.find(c => c.name === hiddenInput.value); + } + if (initialCountry) { const flagEmoji = initialCountry.iso2 .toUpperCase() .split('') .map(char => String.fromCodePoint(127397 + char.charCodeAt(0))) .join(''); - selectNationality(componentId, initialCountry.name, flagEmoji); + selectNationality(componentId, initialCountry.iso3, flagEmoji, initialCountry.name); } } } - function selectNationality(componentId, name, flag) { + function selectNationality(componentId, iso3, flag, displayName) { const flagElement = document.getElementById(componentId + 'SelectedFlag'); const countryElement = document.getElementById(componentId + 'SelectedCountry'); const hiddenInput = document.getElementById(componentId); if (flagElement) flagElement.textContent = flag + ' '; - if (countryElement) countryElement.textContent = name; - if (hiddenInput) hiddenInput.value = name; + if (countryElement) countryElement.textContent = displayName; + if (hiddenInput) hiddenInput.value = iso3; // Close the dropdown after selection const dropdownButton = document.getElementById(componentId + 'Dropdown'); diff --git a/resources/views/components/social-link-row.blade.php b/resources/views/components/social-link-row.blade.php new file mode 100644 index 0000000..859c9a7 --- /dev/null +++ b/resources/views/components/social-link-row.blade.php @@ -0,0 +1,92 @@ +@props(['index', 'link']) + + diff --git a/resources/views/family/edit.blade.php b/resources/views/family/edit.blade.php index fed2c0a..cf3e7ab 100644 --- a/resources/views/family/edit.blade.php +++ b/resources/views/family/edit.blade.php @@ -2,398 +2,15 @@ @section('content')
-
-
-
-
-

Edit Family Member

-
-
- -
-
- @if($relationship->dependent->profile_picture) - Profile Picture - @else -
- {{ strtoupper(substr($relationship->dependent->full_name, 0, 1)) }} -
- @endif -
- -
- -
- @csrf - @method('PUT') - -
- - - @error('full_name') -
{{ $message }}
- @enderror -
- -
- - - @error('email') -
{{ $message }}
- @enderror -
- -
- -
- - - - -
- @error('mobile') -
{{ $message }}
- @enderror - @error('mobile_code') -
{{ $message }}
- @enderror -
- -
-
- - - @error('gender') -
{{ $message }}
- @enderror -
-
- - - @error('birthdate') -
{{ $message }}
- @enderror -
-
- -
-
- - - @error('blood_type') -
{{ $message }}
- @enderror -
-
- -
-
- -
-
- Social Media Links - -
- -
- -
- - -
Share a personal motto or quote that inspires them.
- @error('motto') -
{{ $message }}
- @enderror -
- -
-
- - - @error('relationship_type') -
{{ $message }}
- @enderror -
-
- -
- is_billing_contact) ? 'checked' : '' }}> - -
- -
- Cancel -
- - -
-
-
-
-
-
-
+
- - - - - -@push('scripts') - -@endpush @endsection diff --git a/resources/views/family/partials/affiliations-enhanced.blade.php b/resources/views/family/partials/affiliations-enhanced.blade.php index f7d19c5..b80bac5 100644 --- a/resources/views/family/partials/affiliations-enhanced.blade.php +++ b/resources/views/family/partials/affiliations-enhanced.blade.php @@ -119,12 +119,16 @@
{{ $affiliation->club_name }}
- - {{ $affiliation->start_date->format('M Y') }} - {{ $isOngoing ? 'Present' : $affiliation->end_date->format('M Y') }} - - - {{ $affiliation->formatted_duration }} - + @if($affiliation->start_date) + + {{ $affiliation->start_date->format('M Y') }} - {{ $isOngoing ? 'Present' : ($affiliation->end_date ? $affiliation->end_date->format('M Y') : 'N/A') }} + + @endif + @if($affiliation->formatted_duration) + + {{ $affiliation->formatted_duration }} + + @endif @if($ageAtStart) Age: {{ $ageAtStart }}{{ $ageAtEnd && $ageAtEnd != $ageAtStart ? " to $ageAtEnd" : '' }} @@ -165,7 +169,7 @@ Proficiency: {{ ucfirst($skill->proficiency_level) }}
Duration: {{ $skill->formatted_duration }}
@if($skill->instructor)Instructor: {{ $skill->instructor->user->full_name ?? 'Unknown' }}
@endif - Started: {{ $skill->start_date->format('M Y') }}"> + @if($skill->start_date)Started: {{ $skill->start_date->format('M Y') }}@endif"> {{ $skill->skill_name }} {{ ucfirst($skill->proficiency_level) }} @@ -432,15 +436,18 @@
- {{ $subscription->start_date->format('M d, Y') }} - {{ $subscription->end_date->format('M d, Y') }} + {{ $subscription->start_date ? $subscription->start_date->format('M d, Y') : 'N/A' }} - {{ $subscription->end_date ? $subscription->end_date->format('M d, Y') : 'N/A' }}
@php - $duration = $subscription->start_date->diff($subscription->end_date); - $durationParts = []; - if ($duration->y > 0) $durationParts[] = $duration->y . ' year' . ($duration->y > 1 ? 's' : ''); - if ($duration->m > 0) $durationParts[] = $duration->m . ' month' . ($duration->m > 1 ? 's' : ''); - if ($duration->d > 0) $durationParts[] = $duration->d . ' day' . ($duration->d > 1 ? 's' : ''); - $durationText = implode(' ', $durationParts) ?: 'Same day'; + $durationText = 'N/A'; + if ($subscription->start_date && $subscription->end_date) { + $duration = $subscription->start_date->diff($subscription->end_date); + $durationParts = []; + if ($duration->y > 0) $durationParts[] = $duration->y . ' year' . ($duration->y > 1 ? 's' : ''); + if ($duration->m > 0) $durationParts[] = $duration->m . ' month' . ($duration->m > 1 ? 's' : ''); + if ($duration->d > 0) $durationParts[] = $duration->d . ' day' . ($duration->d > 1 ? 's' : ''); + $durationText = implode(' ', $durationParts) ?: 'Same day'; + } @endphp Duration: {{ $durationText }} @@ -530,13 +537,16 @@
You subscribed to this package {{ $samePackageSubscriptions->count() + 1 }} times:
-
+ @csrf