finally finished edit profile modal

This commit is contained in:
Ghassan Yusuf 2026-01-28 02:07:06 +03:00
parent 0b5b3dd3ee
commit a7434e33d7
25 changed files with 7088 additions and 765 deletions

230
ADMIN_MEMBERS_FIX.md Normal file
View File

@ -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)

View File

@ -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']);
}
}

View File

@ -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();
// 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();
// 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();
// 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,
]);
// Update relationship if it exists (not for admin or own profile)
if (!$isSuperAdmin && !$isOwnProfile && isset($relationship)) {
$relationship->update([
'relationship_type' => $validated['relationship_type'],
'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();
// 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.');
}

View File

@ -0,0 +1,709 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use App\Models\UserRelationship;
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\Auth;
use Illuminate\Support\Facades\Storage;
class MemberController extends Controller
{
/**
* Display a listing of members (family dashboard).
*
* @return \Illuminate\View\View
*/
public function index()
{
$user = Auth::user();
$dependents = UserRelationship::where('guardian_user_id', $user->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.');
}
}

View File

@ -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',
];

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('marital_status')->nullable()->after('gender');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('marital_status');
});
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -32,7 +32,7 @@
data-member-phone="{{ $member->formatted_mobile ?? '' }}"
data-member-nationality="{{ $member->nationality ?? '' }}"
data-member-gender="{{ $member->gender ?? '' }}">
<a href="{{ route('family.show', $member->id) }}" class="text-decoration-none">
<a href="{{ route('member.show', $member->id) }}" class="text-decoration-none">
<div class="card h-100 shadow-sm border overflow-hidden d-flex flex-column family-card">
<!-- Header with gradient background -->
<div class="p-4 pb-3" style="background: linear-gradient(135deg, {{ $member->gender == 'm' ? 'rgba(147, 51, 234, 0.1) 0%, rgba(147, 51, 234, 0.05) 50%' : 'rgba(214, 51, 132, 0.1) 0%, rgba(214, 51, 132, 0.05) 50%' }}, transparent 100%);">
@ -40,7 +40,7 @@
<div class="position-relative">
<div class="rounded-circle border border-4 border-white shadow" style="width: 80px; height: 80px; overflow: hidden; box-shadow: 0 0 0 2px {{ $member->gender == 'm' ? 'rgba(147, 51, 234, 0.3)' : 'rgba(214, 51, 132, 0.3)' }} !important;">
@if($member->profile_picture)
<img src="{{ asset('storage/' . $member->profile_picture) }}" alt="{{ $member->full_name }}" class="w-100 h-100" style="object-fit: cover;">
<img src="{{ asset('storage/' . $member->profile_picture) }}" alt="{{ $member->full_name }}" class="w-100 h-100" style="object-fit: cover; image-rendering: -webkit-optimize-contrast; image-rendering: crisp-edges;">
@else
<div class="w-100 h-100 d-flex align-items-center justify-content-center text-white fw-bold fs-4" style="background: linear-gradient(135deg, {{ $member->gender == 'm' ? '#8b5cf6 0%, #7c3aed 100%' : '#d63384 0%, #a61e4d 100%' }});">
{{ 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;
}
</style>
@endpush

File diff suppressed because it is too large Load Diff

View File

@ -108,7 +108,7 @@
<span>${country.name}</span>
`;
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');

View File

@ -0,0 +1,92 @@
@props(['index', 'link'])
<div class="social-link-row mb-3 d-flex align-items-end">
<div class="me-2 flex-grow-1">
<label class="form-label">Platform</label>
<div class="custom-select-wrapper">
<button type="button" class="form-select text-start custom-select-btn" data-index="{{ $index }}">
@if(($link['platform'] ?? '') == 'facebook')
<i class="bi bi-facebook me-2"></i>Facebook
@elseif(($link['platform'] ?? '') == 'twitter')
<i class="bi bi-twitter-x me-2"></i>Twitter/X
@elseif(($link['platform'] ?? '') == 'instagram')
<i class="bi bi-instagram me-2"></i>Instagram
@elseif(($link['platform'] ?? '') == 'linkedin')
<i class="bi bi-linkedin me-2"></i>LinkedIn
@elseif(($link['platform'] ?? '') == 'youtube')
<i class="bi bi-youtube me-2"></i>YouTube
@elseif(($link['platform'] ?? '') == 'tiktok')
<i class="bi bi-tiktok me-2"></i>TikTok
@elseif(($link['platform'] ?? '') == 'snapchat')
<i class="bi bi-snapchat me-2"></i>Snapchat
@elseif(($link['platform'] ?? '') == 'whatsapp')
<i class="bi bi-whatsapp me-2"></i>WhatsApp
@elseif(($link['platform'] ?? '') == 'telegram')
<i class="bi bi-telegram me-2"></i>Telegram
@elseif(($link['platform'] ?? '') == 'discord')
<i class="bi bi-discord me-2"></i>Discord
@elseif(($link['platform'] ?? '') == 'reddit')
<i class="bi bi-reddit me-2"></i>Reddit
@elseif(($link['platform'] ?? '') == 'pinterest')
<i class="bi bi-pinterest me-2"></i>Pinterest
@elseif(($link['platform'] ?? '') == 'twitch')
<i class="bi bi-twitch me-2"></i>Twitch
@elseif(($link['platform'] ?? '') == 'github')
<i class="bi bi-github me-2"></i>GitHub
@elseif(($link['platform'] ?? '') == 'spotify')
<i class="bi bi-spotify me-2"></i>Spotify
@elseif(($link['platform'] ?? '') == 'skype')
<i class="bi bi-skype me-2"></i>Skype
@elseif(($link['platform'] ?? '') == 'slack')
<i class="bi bi-slack me-2"></i>Slack
@elseif(($link['platform'] ?? '') == 'medium')
<i class="bi bi-medium me-2"></i>Medium
@elseif(($link['platform'] ?? '') == 'vimeo')
<i class="bi bi-vimeo me-2"></i>Vimeo
@elseif(($link['platform'] ?? '') == 'messenger')
<i class="bi bi-messenger me-2"></i>Messenger
@elseif(($link['platform'] ?? '') == 'wechat')
<i class="bi bi-wechat me-2"></i>WeChat
@elseif(($link['platform'] ?? '') == 'line')
<i class="bi bi-line me-2"></i>Line
@else
Select Platform
@endif
</button>
<input type="hidden" name="social_links[{{ $index }}][platform]" value="{{ $link['platform'] ?? '' }}" class="platform-value" required>
<div class="custom-select-dropdown" style="display: none;">
<div class="custom-select-option" data-value="facebook"><i class="bi bi-facebook me-2"></i>Facebook</div>
<div class="custom-select-option" data-value="twitter"><i class="bi bi-twitter-x me-2"></i>Twitter/X</div>
<div class="custom-select-option" data-value="instagram"><i class="bi bi-instagram me-2"></i>Instagram</div>
<div class="custom-select-option" data-value="linkedin"><i class="bi bi-linkedin me-2"></i>LinkedIn</div>
<div class="custom-select-option" data-value="youtube"><i class="bi bi-youtube me-2"></i>YouTube</div>
<div class="custom-select-option" data-value="tiktok"><i class="bi bi-tiktok me-2"></i>TikTok</div>
<div class="custom-select-option" data-value="snapchat"><i class="bi bi-snapchat me-2"></i>Snapchat</div>
<div class="custom-select-option" data-value="whatsapp"><i class="bi bi-whatsapp me-2"></i>WhatsApp</div>
<div class="custom-select-option" data-value="telegram"><i class="bi bi-telegram me-2"></i>Telegram</div>
<div class="custom-select-option" data-value="discord"><i class="bi bi-discord me-2"></i>Discord</div>
<div class="custom-select-option" data-value="reddit"><i class="bi bi-reddit me-2"></i>Reddit</div>
<div class="custom-select-option" data-value="pinterest"><i class="bi bi-pinterest me-2"></i>Pinterest</div>
<div class="custom-select-option" data-value="twitch"><i class="bi bi-twitch me-2"></i>Twitch</div>
<div class="custom-select-option" data-value="github"><i class="bi bi-github me-2"></i>GitHub</div>
<div class="custom-select-option" data-value="spotify"><i class="bi bi-spotify me-2"></i>Spotify</div>
<div class="custom-select-option" data-value="skype"><i class="bi bi-skype me-2"></i>Skype</div>
<div class="custom-select-option" data-value="slack"><i class="bi bi-slack me-2"></i>Slack</div>
<div class="custom-select-option" data-value="medium"><i class="bi bi-medium me-2"></i>Medium</div>
<div class="custom-select-option" data-value="vimeo"><i class="bi bi-vimeo me-2"></i>Vimeo</div>
<div class="custom-select-option" data-value="messenger"><i class="bi bi-messenger me-2"></i>Messenger</div>
<div class="custom-select-option" data-value="wechat"><i class="bi bi-wechat me-2"></i>WeChat</div>
<div class="custom-select-option" data-value="line"><i class="bi bi-line me-2"></i>Line</div>
</div>
</div>
</div>
<div class="me-2 flex-grow-1">
<label class="form-label">URL</label>
<input type="url" class="form-control" name="social_links[{{ $index }}][url]" value="{{ $link['url'] ?? '' }}" placeholder="https://example.com/username" required>
</div>
<div class="mb-0">
<button type="button" class="btn btn-outline-danger btn-sm remove-social-link">
<i class="bi bi-trash"></i>
</button>
</div>
</div>

View File

@ -2,398 +2,15 @@
@section('content')
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow-sm">
<div class="card-header bg-white">
<h4 class="mb-0">Edit Family Member</h4>
</div>
<div class="card-body">
<!-- Profile Picture Section -->
<div class="mb-4 text-center">
<div class="mb-3">
@if($relationship->dependent->profile_picture)
<img src="{{ asset('storage/' . $relationship->dependent->profile_picture) }}"
alt="Profile Picture"
class="rounded-circle"
style="width: 120px; height: 120px; object-fit: cover; border: 3px solid #dee2e6;">
@else
<div class="rounded-circle d-inline-flex align-items-center justify-content-center text-white fw-bold"
style="width: 120px; height: 120px; font-size: 3rem; background: linear-gradient(135deg, {{ $relationship->dependent->gender == 'm' ? '#0d6efd 0%, #0a58ca 100%' : '#d63384 0%, #a61e4d 100%' }}); border: 3px solid #dee2e6;">
{{ strtoupper(substr($relationship->dependent->full_name, 0, 1)) }}
</div>
@endif
</div>
<x-takeone-cropper
id="family_member_{{ $relationship->dependent->id }}"
width="300"
height="400"
shape="square"
folder="images/profiles"
filename="profile_{{ $relationship->dependent->id }}"
uploadUrl="{{ route('family.upload-picture', $relationship->dependent->id) }}"
<x-edit-profile-modal
:user="$relationship->dependent"
:formAction="$relationship->relationship_type === 'admin_view' ? route('admin.platform.members.update', $relationship->dependent->id) : route('family.update', $relationship->dependent->id)"
formMethod="PUT"
:cancelUrl="$relationship->relationship_type === 'admin_view' ? route('admin.platform.members') : route('family.dashboard')"
:uploadUrl="$relationship->relationship_type === 'admin_view' ? route('admin.platform.members.upload-picture', $relationship->dependent->id) : route('family.upload-picture', $relationship->dependent->id)"
:showRelationshipFields="$relationship->relationship_type !== 'admin_view' && $relationship->relationship_type !== 'self'"
:relationship="$relationship"
/>
</div>
<form method="POST" action="{{ route('family.update', $relationship->dependent->id) }}">
@csrf
@method('PUT')
<div class="mb-3">
<label for="full_name" class="form-label">Full Name</label>
<input type="text" class="form-control @error('full_name') is-invalid @enderror" id="full_name" name="full_name" value="{{ old('full_name', $relationship->dependent->full_name) }}" required>
@error('full_name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="email" class="form-label">Email Address <span class="text-muted">(Optional for children)</span></label>
<input type="email" class="form-control @error('email') is-invalid @enderror" id="email" name="email" value="{{ old('email', $relationship->dependent->email) }}">
@error('email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="mobile" class="form-label">Mobile Number</label>
<div class="input-group" onclick="event.stopPropagation()">
<button class="btn btn-outline-secondary dropdown-toggle country-dropdown-btn d-flex align-items-center" type="button" id="country_codeDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<span class="fi fi-bh me-2" id="country_codeSelectedFlag"></span>
<span class="country-label" id="country_codeSelectedCountry">{{ old('mobile_code', $relationship->dependent->mobile['code'] ?? '+973') }}</span>
</button>
<div class="dropdown-menu p-2" aria-labelledby="country_codeDropdown" style="min-width: 200px;" onclick="event.stopPropagation()">
<input type="text" class="form-control form-control-sm mb-2" placeholder="Search country..." id="country_codeSearch" onkeydown="event.stopPropagation()">
<div class="country-list" id="country_codeList" style="max-height: 300px; overflow-y: auto;"></div>
</div>
<input type="hidden" id="country_code" name="mobile_code" value="{{ old('mobile_code', $relationship->dependent->mobile['code'] ?? '+973') }}">
<input id="mobile_number" type="tel" class="form-control @error('mobile') is-invalid @enderror" name="mobile" value="{{ old('mobile', $relationship->dependent->mobile['number'] ?? '') }}" autocomplete="tel" placeholder="Phone number">
</div>
@error('mobile')
<div class="invalid-feedback d-block">{{ $message }}</div>
@enderror
@error('mobile_code')
<div class="invalid-feedback d-block">{{ $message }}</div>
@enderror
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="gender" class="form-label">Gender</label>
<select class="form-select @error('gender') is-invalid @enderror" id="gender" name="gender" required>
<option value="">Select Gender</option>
<option value="m" {{ old('gender', $relationship->dependent->gender) == 'm' ? 'selected' : '' }}>Male</option>
<option value="f" {{ old('gender', $relationship->dependent->gender) == 'f' ? 'selected' : '' }}>Female</option>
</select>
@error('gender')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-md-6">
<label for="birthdate" class="form-label">Birthdate</label>
<input type="date" class="form-control @error('birthdate') is-invalid @enderror" id="birthdate" name="birthdate" value="{{ old('birthdate', $relationship->dependent->birthdate->format('Y-m-d')) }}" required>
@error('birthdate')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="blood_type" class="form-label">Blood Type</label>
<select class="form-select @error('blood_type') is-invalid @enderror" id="blood_type" name="blood_type">
<option value="">Select Blood Type</option>
<option value="A+" {{ old('blood_type', $relationship->dependent->blood_type) == 'A+' ? 'selected' : '' }}>A+</option>
<option value="A-" {{ old('blood_type', $relationship->dependent->blood_type) == 'A-' ? 'selected' : '' }}>A-</option>
<option value="B+" {{ old('blood_type', $relationship->dependent->blood_type) == 'B+' ? 'selected' : '' }}>B+</option>
<option value="B-" {{ old('blood_type', $relationship->dependent->blood_type) == 'B-' ? 'selected' : '' }}>B-</option>
<option value="AB+" {{ old('blood_type', $relationship->dependent->blood_type) == 'AB+' ? 'selected' : '' }}>AB+</option>
<option value="AB-" {{ old('blood_type', $relationship->dependent->blood_type) == 'AB-' ? 'selected' : '' }}>AB-</option>
<option value="O+" {{ old('blood_type', $relationship->dependent->blood_type) == 'O+' ? 'selected' : '' }}>O+</option>
<option value="O-" {{ old('blood_type', $relationship->dependent->blood_type) == 'O-' ? 'selected' : '' }}>O-</option>
<option value="Unknown" {{ old('blood_type', $relationship->dependent->blood_type) == 'Unknown' ? 'selected' : '' }}>Unknown</option>
</select>
@error('blood_type')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-md-6">
<x-country-dropdown
name="nationality"
id="nationality"
:value="old('nationality', $relationship->dependent->nationality)"
:required="true"
:error="$errors->first('nationality')" />
</div>
</div>
<div class="mb-3">
<h5 class="form-label d-flex justify-content-between align-items-center">
Social Media Links
<button type="button" class="btn btn-outline-primary btn-sm" id="addSocialLink">
<i class="bi bi-plus"></i> Add Link
</button>
</h5>
<div id="socialLinksContainer">
@php
$existingLinks = old('social_links', $relationship->dependent->social_links ?? []);
if (!is_array($existingLinks)) {
$existingLinks = [];
}
// Convert associative array to array of arrays for form display
$formLinks = [];
foreach ($existingLinks as $platform => $url) {
$formLinks[] = ['platform' => $platform, 'url' => $url];
}
@endphp
@foreach($formLinks as $index => $link)
<div class="social-link-row mb-3 d-flex align-items-end">
<div class="me-2 flex-grow-1">
<label class="form-label">Platform</label>
<select class="form-select platform-select" name="social_links[{{ $index }}][platform]" required>
<option value="">Select Platform</option>
<option value="facebook" {{ ($link['platform'] ?? '') == 'facebook' ? 'selected' : '' }}>Facebook</option>
<option value="twitter" {{ ($link['platform'] ?? '') == 'twitter' ? 'selected' : '' }}>Twitter/X</option>
<option value="instagram" {{ ($link['platform'] ?? '') == 'instagram' ? 'selected' : '' }}>Instagram</option>
<option value="linkedin" {{ ($link['platform'] ?? '') == 'linkedin' ? 'selected' : '' }}>LinkedIn</option>
<option value="youtube" {{ ($link['platform'] ?? '') == 'youtube' ? 'selected' : '' }}>YouTube</option>
<option value="tiktok" {{ ($link['platform'] ?? '') == 'tiktok' ? 'selected' : '' }}>TikTok</option>
<option value="snapchat" {{ ($link['platform'] ?? '') == 'snapchat' ? 'selected' : '' }}>Snapchat</option>
<option value="whatsapp" {{ ($link['platform'] ?? '') == 'whatsapp' ? 'selected' : '' }}>WhatsApp</option>
<option value="telegram" {{ ($link['platform'] ?? '') == 'telegram' ? 'selected' : '' }}>Telegram</option>
<option value="discord" {{ ($link['platform'] ?? '') == 'discord' ? 'selected' : '' }}>Discord</option>
<option value="reddit" {{ ($link['platform'] ?? '') == 'reddit' ? 'selected' : '' }}>Reddit</option>
<option value="pinterest" {{ ($link['platform'] ?? '') == 'pinterest' ? 'selected' : '' }}>Pinterest</option>
<option value="twitch" {{ ($link['platform'] ?? '') == 'twitch' ? 'selected' : '' }}>Twitch</option>
<option value="github" {{ ($link['platform'] ?? '') == 'github' ? 'selected' : '' }}>GitHub</option>
<option value="spotify" {{ ($link['platform'] ?? '') == 'spotify' ? 'selected' : '' }}>Spotify</option>
<option value="skype" {{ ($link['platform'] ?? '') == 'skype' ? 'selected' : '' }}>Skype</option>
<option value="slack" {{ ($link['platform'] ?? '') == 'slack' ? 'selected' : '' }}>Slack</option>
<option value="medium" {{ ($link['platform'] ?? '') == 'medium' ? 'selected' : '' }}>Medium</option>
<option value="vimeo" {{ ($link['platform'] ?? '') == 'vimeo' ? 'selected' : '' }}>Vimeo</option>
<option value="messenger" {{ ($link['platform'] ?? '') == 'messenger' ? 'selected' : '' }}>Messenger</option>
<option value="wechat" {{ ($link['platform'] ?? '') == 'wechat' ? 'selected' : '' }}>WeChat</option>
<option value="line" {{ ($link['platform'] ?? '') == 'line' ? 'selected' : '' }}>Line</option>
</select>
</div>
<div class="me-2 flex-grow-1">
<label class="form-label">URL</label>
<input type="url" class="form-control" name="social_links[{{ $index }}][url]" value="{{ $link['url'] ?? '' }}" placeholder="https://example.com/username" required>
</div>
<div class="mb-0">
<button type="button" class="btn btn-outline-danger btn-sm remove-social-link">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
@endforeach
</div>
</div>
<div class="mb-3">
<label for="motto" class="form-label">Personal Motto</label>
<textarea class="form-control @error('motto') is-invalid @enderror" id="motto" name="motto" rows="3" placeholder="Enter personal motto or quote...">{{ old('motto', $relationship->dependent->motto) }}</textarea>
<div class="form-text">Share a personal motto or quote that inspires them.</div>
@error('motto')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="relationship_type" class="form-label">Relationship</label>
<select class="form-select @error('relationship_type') is-invalid @enderror" id="relationship_type" name="relationship_type" required>
<option value="">Select Relationship</option>
<option value="son" {{ old('relationship_type', $relationship->relationship_type) == 'son' ? 'selected' : '' }}>Son</option>
<option value="daughter" {{ old('relationship_type', $relationship->relationship_type) == 'daughter' ? 'selected' : '' }}>Daughter</option>
<option value="spouse" {{ old('relationship_type', $relationship->relationship_type) == 'spouse' ? 'selected' : '' }}>Wife</option>
<option value="sponsor" {{ old('relationship_type', $relationship->relationship_type) == 'sponsor' ? 'selected' : '' }}>Sponsor</option>
<option value="other" {{ old('relationship_type', $relationship->relationship_type) == 'other' ? 'selected' : '' }}>Other</option>
</select>
@error('relationship_type')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="is_billing_contact" name="is_billing_contact" value="1" {{ old('is_billing_contact', $relationship->is_billing_contact) ? 'checked' : '' }}>
<label class="form-check-label" for="is_billing_contact">Is Billing Contact</label>
</div>
<div class="d-flex justify-content-between">
<a href="{{ route('family.dashboard') }}" class="btn btn-outline-secondary">Cancel</a>
<div>
<button type="button" class="btn btn-danger me-2" data-bs-toggle="modal" data-bs-target="#deleteModal">
Remove
</button>
<button type="submit" class="btn btn-primary">Update</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">Confirm Removal</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Are you sure you want to remove {{ $relationship->dependent->full_name }} from your family?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<form action="{{ route('family.destroy', $relationship->dependent->id) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger">Remove</button>
</form>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
let socialLinkIndex = {{ count($formLinks ?? []) }};
// Add new social link row
document.getElementById('addSocialLink').addEventListener('click', function() {
addSocialLinkRow();
});
// Remove social link row
document.addEventListener('click', function(e) {
if (e.target.classList.contains('remove-social-link') || e.target.closest('.remove-social-link')) {
e.target.closest('.social-link-row').remove();
}
});
function addSocialLinkRow(platform = '', url = '') {
const container = document.getElementById('socialLinksContainer');
const row = document.createElement('div');
row.className = 'social-link-row mb-3 d-flex align-items-end';
row.innerHTML = `
<div class="me-2 flex-grow-1">
<label class="form-label">Platform</label>
<select class="form-select platform-select" name="social_links[${socialLinkIndex}][platform]" required>
<option value="">Select Platform</option>
<option value="facebook" ${platform === 'facebook' ? 'selected' : ''}>Facebook</option>
<option value="twitter" ${platform === 'twitter' ? 'selected' : ''}>Twitter/X</option>
<option value="instagram" ${platform === 'instagram' ? 'selected' : ''}>Instagram</option>
<option value="linkedin" ${platform === 'linkedin' ? 'selected' : ''}>LinkedIn</option>
<option value="youtube" ${platform === 'youtube' ? 'selected' : ''}>YouTube</option>
<option value="tiktok" ${platform === 'tiktok' ? 'selected' : ''}>TikTok</option>
<option value="snapchat" ${platform === 'snapchat' ? 'selected' : ''}>Snapchat</option>
<option value="whatsapp" ${platform === 'whatsapp' ? 'selected' : ''}>WhatsApp</option>
<option value="telegram" ${platform === 'telegram' ? 'selected' : ''}>Telegram</option>
<option value="discord" ${platform === 'discord' ? 'selected' : ''}>Discord</option>
<option value="reddit" ${platform === 'reddit' ? 'selected' : ''}>Reddit</option>
<option value="pinterest" ${platform === 'pinterest' ? 'selected' : ''}>Pinterest</option>
<option value="twitch" ${platform === 'twitch' ? 'selected' : ''}>Twitch</option>
<option value="github" ${platform === 'github' ? 'selected' : ''}>GitHub</option>
<option value="spotify" ${platform === 'spotify' ? 'selected' : ''}>Spotify</option>
<option value="skype" ${platform === 'skype' ? 'selected' : ''}>Skype</option>
<option value="slack" ${platform === 'slack' ? 'selected' : ''}>Slack</option>
<option value="medium" ${platform === 'medium' ? 'selected' : ''}>Medium</option>
<option value="vimeo" ${platform === 'vimeo' ? 'selected' : ''}>Vimeo</option>
<option value="messenger" ${platform === 'messenger' ? 'selected' : ''}>Messenger</option>
<option value="wechat" ${platform === 'wechat' ? 'selected' : ''}>WeChat</option>
<option value="line" ${platform === 'line' ? 'selected' : ''}>Line</option>
</select>
</div>
<div class="me-2 flex-grow-1">
<label class="form-label">URL</label>
<input type="url" class="form-control" name="social_links[${socialLinkIndex}][url]" value="${url}" placeholder="https://example.com/username" required>
</div>
<div class="mb-0">
<button type="button" class="btn btn-outline-danger btn-sm remove-social-link">
<i class="bi bi-trash"></i>
</button>
</div>
`;
container.appendChild(row);
socialLinkIndex++;
}
});
</script>
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
// Load countries for mobile code dropdown
fetch('/data/countries.json')
.then(response => response.json())
.then(countries => {
const listElement = document.getElementById('country_codeList');
const selectedFlag = document.getElementById('country_codeSelectedFlag');
const selectedCountry = document.getElementById('country_codeSelectedCountry');
const hiddenInput = document.getElementById('country_code');
const searchInput = document.getElementById('country_codeSearch');
if (!listElement) return;
// Populate dropdown
countries.forEach(country => {
const button = document.createElement('button');
button.className = 'dropdown-item d-flex align-items-center';
button.type = 'button';
button.setAttribute('data-country-code', country.call_code);
button.setAttribute('data-country-name', country.name);
button.setAttribute('data-flag-code', country.flag);
button.setAttribute('data-search', `${country.name.toLowerCase()} ${country.call_code}`);
button.innerHTML = `<span class="fi fi-${country.flag.toLowerCase()} me-2"></span><span>${country.name} (${country.call_code})</span>`;
button.addEventListener('click', function() {
const code = this.getAttribute('data-country-code');
const flag = this.getAttribute('data-flag-code');
const name = this.getAttribute('data-country-name');
selectedFlag.className = `fi fi-${flag.toLowerCase()} me-2`;
selectedCountry.textContent = code;
hiddenInput.value = code;
// Close dropdown
const dropdown = bootstrap.Dropdown.getInstance(document.getElementById('country_codeDropdown'));
if (dropdown) dropdown.hide();
});
listElement.appendChild(button);
});
// Set initial value
const initialValue = '{{ old('mobile_code', $relationship->dependent->mobile['code'] ?? '+973') }}';
if (initialValue) {
hiddenInput.value = initialValue;
selectedCountry.textContent = initialValue;
const country = countries.find(c => c.call_code === initialValue);
if (country) {
selectedFlag.className = `fi fi-${country.flag.toLowerCase()} me-2`;
}
}
// Search functionality
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const items = listElement.querySelectorAll('.dropdown-item');
items.forEach(item => {
const searchData = item.getAttribute('data-search');
if (searchData.includes(searchTerm)) {
item.style.display = '';
} else {
item.style.display = 'none';
}
});
});
})
.catch(error => console.error('Error loading countries:', error));
});
</script>
@endpush
@endsection

View File

@ -119,12 +119,16 @@
<div class="flex-grow-1 text-white">
<h5 class="mb-1 fw-bold">{{ $affiliation->club_name }}</h5>
<div class="d-flex gap-3 flex-wrap">
@if($affiliation->start_date)
<small class="opacity-90">
<i class="bi bi-calendar-event me-1"></i>{{ $affiliation->start_date->format('M Y') }} - {{ $isOngoing ? 'Present' : $affiliation->end_date->format('M Y') }}
<i class="bi bi-calendar-event me-1"></i>{{ $affiliation->start_date->format('M Y') }} - {{ $isOngoing ? 'Present' : ($affiliation->end_date ? $affiliation->end_date->format('M Y') : 'N/A') }}
</small>
@endif
@if($affiliation->formatted_duration)
<small class="opacity-90">
<i class="bi bi-hourglass-split me-1"></i>{{ $affiliation->formatted_duration }}
</small>
@endif
@if($ageAtStart)
<small class="opacity-90">
<i class="bi bi-person me-1"></i>Age: {{ $ageAtStart }}{{ $ageAtEnd && $ageAtEnd != $ageAtStart ? " to $ageAtEnd" : '' }}
@ -165,7 +169,7 @@
Proficiency: {{ ucfirst($skill->proficiency_level) }}<br>
Duration: {{ $skill->formatted_duration }}<br>
@if($skill->instructor)Instructor: {{ $skill->instructor->user->full_name ?? 'Unknown' }}<br>@endif
Started: {{ $skill->start_date->format('M Y') }}">
@if($skill->start_date)Started: {{ $skill->start_date->format('M Y') }}@endif">
<i class="bi bi-star-fill me-1"></i>{{ $skill->skill_name }}
<span class="badge bg-white text-dark ms-1" style="font-size: 0.65rem;">{{ ucfirst($skill->proficiency_level) }}</span>
</span>
@ -432,15 +436,18 @@
<label class="text-muted small fw-semibold">Subscription Period</label>
<div>
<i class="bi bi-calendar-range me-2 text-primary"></i>
{{ $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' }}
</div>
@php
$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
<small class="text-muted">
<i class="bi bi-hourglass-split me-1"></i>Duration: {{ $durationText }}
@ -530,13 +537,16 @@
<div class="fw-semibold mb-1">You subscribed to this package {{ $samePackageSubscriptions->count() + 1 }} times:</div>
<ul class="mb-0 ps-3">
<li class="text-primary fw-semibold">
{{ $subscription->start_date->format('M d, Y') }} - {{ $subscription->end_date->format('M d, Y') }} (Current)
{{ $subscription->start_date ? $subscription->start_date->format('M d, Y') : 'N/A' }} - {{ $subscription->end_date ? $subscription->end_date->format('M d, Y') : 'N/A' }} (Current)
</li>
@foreach($samePackageSubscriptions as $otherSub)
<li>
{{ $otherSub->start_date->format('M d, Y') }} - {{ $otherSub->end_date->format('M d, Y') }}
{{ $otherSub->start_date ? $otherSub->start_date->format('M d, Y') : 'N/A' }} - {{ $otherSub->end_date ? $otherSub->end_date->format('M d, Y') : 'N/A' }}
@php
$gap = 0;
if ($subscription->start_date && $otherSub->start_date) {
$gap = $subscription->start_date->diffInMonths($otherSub->start_date);
}
@endphp
@if($gap > 0)
<small class="text-muted">({{ abs($gap) }} months {{ $subscription->start_date->gt($otherSub->start_date) ? 'before' : 'after' }} current)</small>

View File

@ -2,291 +2,13 @@
@section('content')
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow-sm">
<div class="card-header bg-white">
<h4 class="mb-0">Edit Profile</h4>
</div>
<div class="card-body">
<!-- Profile Picture Section -->
@php
$currentProfileImage = '';
// Check user's profile_picture field first (set by upload controller)
if ($user->profile_picture && file_exists(public_path('storage/' . $user->profile_picture))) {
$currentProfileImage = asset('storage/' . $user->profile_picture);
} else {
// Fallback: check for files with common extensions
$extensions = ['png', 'jpg', 'jpeg', 'webp'];
foreach ($extensions as $ext) {
$path = 'storage/images/profiles/profile_' . $user->id . '.' . $ext;
if (file_exists(public_path($path))) {
$currentProfileImage = asset($path);
break;
}
}
}
@endphp
<div class="mb-4">
<x-image-upload
id="profile_picture"
width="300"
height="400"
shape="square"
folder="images/profiles"
filename="profile_{{ $user->id }}"
uploadUrl="{{ route('profile.upload-picture') }}"
currentImage="{{ $currentProfileImage }}"
placeholder="No profile picture"
placeholderIcon="bi-person-circle"
buttonText="Change Photo"
<x-edit-profile-modal
:user="$user"
:formAction="route('profile.update')"
formMethod="PUT"
:cancelUrl="route('profile.show')"
:uploadUrl="route('profile.upload-picture')"
/>
</div>
<form method="POST" action="{{ route('profile.update') }}">
@csrf
@method('PUT')
<div class="mb-3">
<label for="full_name" class="form-label">Full Name</label>
<input type="text" class="form-control @error('full_name') is-invalid @enderror" id="full_name" name="full_name" value="{{ old('full_name', $user->full_name) }}" required>
@error('full_name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<input type="email" class="form-control @error('email') is-invalid @enderror" id="email" name="email" value="{{ old('email', $user->email) }}" required>
@error('email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="mobile_number" class="form-label">Mobile Number</label>
<x-country-code-dropdown
name="mobile_code"
id="country_code"
:value="old('mobile_code', $user->mobile['code'] ?? '+973')"
:required="true"
:error="$errors->first('mobile_code')">
<input id="mobile_number" type="tel"
class="form-control @error('mobile') is-invalid @enderror"
name="mobile"
value="{{ old('mobile', $user->mobile['number'] ?? '') }}"
required autocomplete="tel"
placeholder="Phone number">
</x-country-code-dropdown>
@error('mobile')
<div class="invalid-feedback d-block">{{ $message }}</div>
@enderror
</div>
<div class="row mb-3">
<div class="col-md-6">
<x-gender-dropdown
name="gender"
id="gender"
:value="old('gender', $user->gender)"
:required="true"
:error="$errors->first('gender')" />
</div>
<div class="col-md-6">
<x-birthdate-dropdown
name="birthdate"
id="birthdate"
label="Birthdate"
:value="old('birthdate', $user->birthdate?->format('Y-m-d'))"
:required="true"
:min-age="10"
:max-age="120"
:error="$errors->first('birthdate')" />
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="blood_type" class="form-label">Blood Type</label>
<select class="form-select @error('blood_type') is-invalid @enderror" id="blood_type" name="blood_type">
<option value="">Select Blood Type</option>
<option value="A+" {{ old('blood_type', $user->blood_type) == 'A+' ? 'selected' : '' }}>A+</option>
<option value="A-" {{ old('blood_type', $user->blood_type) == 'A-' ? 'selected' : '' }}>A-</option>
<option value="B+" {{ old('blood_type', $user->blood_type) == 'B+' ? 'selected' : '' }}>B+</option>
<option value="B-" {{ old('blood_type', $user->blood_type) == 'B-' ? 'selected' : '' }}>B-</option>
<option value="AB+" {{ old('blood_type', $user->blood_type) == 'AB+' ? 'selected' : '' }}>AB+</option>
<option value="AB-" {{ old('blood_type', $user->blood_type) == 'AB-' ? 'selected' : '' }}>AB-</option>
<option value="O+" {{ old('blood_type', $user->blood_type) == 'O+' ? 'selected' : '' }}>O+</option>
<option value="O-" {{ old('blood_type', $user->blood_type) == 'O-' ? 'selected' : '' }}>O-</option>
<option value="Unknown" {{ old('blood_type', $user->blood_type) == 'Unknown' ? 'selected' : '' }}>Unknown</option>
</select>
@error('blood_type')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-md-6">
<x-nationality-dropdown
name="nationality"
id="nationality"
:value="old('nationality', $user->nationality)"
:required="true"
:error="$errors->first('nationality')" />
</div>
</div>
<div class="mb-3">
<h5 class="form-label d-flex justify-content-between align-items-center">
Social Media Links
<button type="button" class="btn btn-outline-primary btn-sm" id="addSocialLink">
<i class="bi bi-plus"></i> Add Link
</button>
</h5>
<div id="socialLinksContainer">
@php
$existingLinks = old('social_links', $user->social_links ?? []);
if (!is_array($existingLinks)) {
$existingLinks = [];
}
// Convert associative array to array of arrays for form display
$formLinks = [];
foreach ($existingLinks as $platform => $url) {
$formLinks[] = ['platform' => $platform, 'url' => $url];
}
@endphp
@foreach($formLinks as $index => $link)
<div class="social-link-row mb-3 d-flex align-items-end">
<div class="me-2 flex-grow-1">
<label class="form-label">Platform</label>
<select class="form-select platform-select" name="social_links[{{ $index }}][platform]" required>
<option value="">Select Platform</option>
<option value="facebook" {{ ($link['platform'] ?? '') == 'facebook' ? 'selected' : '' }}>Facebook</option>
<option value="twitter" {{ ($link['platform'] ?? '') == 'twitter' ? 'selected' : '' }}>Twitter/X</option>
<option value="instagram" {{ ($link['platform'] ?? '') == 'instagram' ? 'selected' : '' }}>Instagram</option>
<option value="linkedin" {{ ($link['platform'] ?? '') == 'linkedin' ? 'selected' : '' }}>LinkedIn</option>
<option value="youtube" {{ ($link['platform'] ?? '') == 'youtube' ? 'selected' : '' }}>YouTube</option>
<option value="tiktok" {{ ($link['platform'] ?? '') == 'tiktok' ? 'selected' : '' }}>TikTok</option>
<option value="snapchat" {{ ($link['platform'] ?? '') == 'snapchat' ? 'selected' : '' }}>Snapchat</option>
<option value="whatsapp" {{ ($link['platform'] ?? '') == 'whatsapp' ? 'selected' : '' }}>WhatsApp</option>
<option value="telegram" {{ ($link['platform'] ?? '') == 'telegram' ? 'selected' : '' }}>Telegram</option>
<option value="discord" {{ ($link['platform'] ?? '') == 'discord' ? 'selected' : '' }}>Discord</option>
<option value="reddit" {{ ($link['platform'] ?? '') == 'reddit' ? 'selected' : '' }}>Reddit</option>
<option value="pinterest" {{ ($link['platform'] ?? '') == 'pinterest' ? 'selected' : '' }}>Pinterest</option>
<option value="twitch" {{ ($link['platform'] ?? '') == 'twitch' ? 'selected' : '' }}>Twitch</option>
<option value="github" {{ ($link['platform'] ?? '') == 'github' ? 'selected' : '' }}>GitHub</option>
<option value="spotify" {{ ($link['platform'] ?? '') == 'spotify' ? 'selected' : '' }}>Spotify</option>
<option value="skype" {{ ($link['platform'] ?? '') == 'skype' ? 'selected' : '' }}>Skype</option>
<option value="slack" {{ ($link['platform'] ?? '') == 'slack' ? 'selected' : '' }}>Slack</option>
<option value="medium" {{ ($link['platform'] ?? '') == 'medium' ? 'selected' : '' }}>Medium</option>
<option value="vimeo" {{ ($link['platform'] ?? '') == 'vimeo' ? 'selected' : '' }}>Vimeo</option>
<option value="messenger" {{ ($link['platform'] ?? '') == 'messenger' ? 'selected' : '' }}>Messenger</option>
<option value="wechat" {{ ($link['platform'] ?? '') == 'wechat' ? 'selected' : '' }}>WeChat</option>
<option value="line" {{ ($link['platform'] ?? '') == 'line' ? 'selected' : '' }}>Line</option>
</select>
</div>
<div class="me-2 flex-grow-1">
<label class="form-label">URL</label>
<input type="url" class="form-control" name="social_links[{{ $index }}][url]" value="{{ $link['url'] ?? '' }}" placeholder="https://example.com/username" required>
</div>
<div class="mb-0">
<button type="button" class="btn btn-outline-danger btn-sm remove-social-link">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
@endforeach
</div>
</div>
<div class="mb-3">
<label for="motto" class="form-label">Personal Motto</label>
<textarea class="form-control @error('motto') is-invalid @enderror" id="motto" name="motto" rows="3" placeholder="Enter your personal motto or quote...">{{ old('motto', $user->motto) }}</textarea>
<div class="form-text">Share a personal motto or quote that inspires you.</div>
@error('motto')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="d-flex justify-content-between">
<a href="{{ route('profile.show') }}" class="btn btn-outline-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">Update</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
let socialLinkIndex = {{ count($formLinks ?? []) }};
// Add new social link row
document.getElementById('addSocialLink').addEventListener('click', function() {
addSocialLinkRow();
});
// Remove social link row
document.addEventListener('click', function(e) {
if (e.target.classList.contains('remove-social-link') || e.target.closest('.remove-social-link')) {
e.target.closest('.social-link-row').remove();
}
});
function addSocialLinkRow(platform = '', url = '') {
const container = document.getElementById('socialLinksContainer');
const row = document.createElement('div');
row.className = 'social-link-row mb-3 d-flex align-items-end';
row.innerHTML = `
<div class="me-2 flex-grow-1">
<label class="form-label">Platform</label>
<select class="form-select platform-select" name="social_links[${socialLinkIndex}][platform]" required>
<option value="">Select Platform</option>
<option value="facebook" ${platform === 'facebook' ? 'selected' : ''}>Facebook</option>
<option value="twitter" ${platform === 'twitter' ? 'selected' : ''}>Twitter/X</option>
<option value="instagram" ${platform === 'instagram' ? 'selected' : ''}>Instagram</option>
<option value="linkedin" ${platform === 'linkedin' ? 'selected' : ''}>LinkedIn</option>
<option value="youtube" ${platform === 'youtube' ? 'selected' : ''}>YouTube</option>
<option value="tiktok" ${platform === 'tiktok' ? 'selected' : ''}>TikTok</option>
<option value="snapchat" ${platform === 'snapchat' ? 'selected' : ''}>Snapchat</option>
<option value="whatsapp" ${platform === 'whatsapp' ? 'selected' : ''}>WhatsApp</option>
<option value="telegram" ${platform === 'telegram' ? 'selected' : ''}>Telegram</option>
<option value="discord" ${platform === 'discord' ? 'selected' : ''}>Discord</option>
<option value="reddit" ${platform === 'reddit' ? 'selected' : ''}>Reddit</option>
<option value="pinterest" ${platform === 'pinterest' ? 'selected' : ''}>Pinterest</option>
<option value="twitch" ${platform === 'twitch' ? 'selected' : ''}>Twitch</option>
<option value="github" ${platform === 'github' ? 'selected' : ''}>GitHub</option>
<option value="spotify" ${platform === 'spotify' ? 'selected' : ''}>Spotify</option>
<option value="skype" ${platform === 'skype' ? 'selected' : ''}>Skype</option>
<option value="slack" ${platform === 'slack' ? 'selected' : ''}>Slack</option>
<option value="medium" ${platform === 'medium' ? 'selected' : ''}>Medium</option>
<option value="vimeo" ${platform === 'vimeo' ? 'selected' : ''}>Vimeo</option>
<option value="messenger" ${platform === 'messenger' ? 'selected' : ''}>Messenger</option>
<option value="wechat" ${platform === 'wechat' ? 'selected' : ''}>WeChat</option>
<option value="line" ${platform === 'line' ? 'selected' : ''}>Line</option>
</select>
</div>
<div class="me-2 flex-grow-1">
<label class="form-label">URL</label>
<input type="url" class="form-control" name="social_links[${socialLinkIndex}][url]" value="${url}" placeholder="https://example.com/username" required>
</div>
<div class="mb-0">
<button type="button" class="btn btn-outline-danger btn-sm remove-social-link">
<i class="bi bi-trash"></i>
</button>
</div>
`;
container.appendChild(row);
socialLinkIndex++;
}
});
</script>
@endpush
@stack('styles')
@endsection

View File

@ -15,19 +15,6 @@
@section('content')
<div class="container py-4">
<!-- Flash Messages -->
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
{{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
@ -67,7 +54,7 @@
<li><a class="dropdown-item" href="#"><i class="bi bi-calendar-event me-2"></i>Add Event Participation</a></li>
<li><a class="dropdown-item" href="#" data-bs-target="#healthUpdateModal"><i class="bi bi-heart-pulse me-2"></i>Add Health Update</a></li>
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#tournamentParticipationModal"><i class="bi bi-award me-2"></i>Add Tournament Participation</a></li>
<li><a class="dropdown-item" href="@if($relationship->relationship_type == 'self'){{ route('profile.edit') }}@else{{ route('family.edit', $relationship->dependent->id) }}@endif">
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#editProfileModal">
<i class="bi bi-pencil me-2"></i>Edit Info
</a></li>
<li><a class="dropdown-item" href="#"><i class="bi bi-bullseye me-2"></i>Set a Goal</a></li>
@ -1241,7 +1228,7 @@
<h5 class="modal-title" id="healthUpdateModalLabel">Add Health Update</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="healthUpdateForm" method="POST" action="{{ route('family.store-health', $relationship->dependent->id) }}">
<form id="healthUpdateForm" method="POST" action="{{ $relationship->relationship_type === 'admin_view' ? route('admin.platform.members.store-health', $relationship->dependent->id) : route('family.store-health', $relationship->dependent->id) }}">
@csrf
<div class="modal-body">
<div class="row g-3">
@ -1312,7 +1299,7 @@
<h5 class="modal-title" id="tournamentParticipationModalLabel">Add Tournament Participation</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="tournamentParticipationForm" method="POST" action="{{ route('family.store-tournament', $relationship->dependent->id) }}">
<form id="tournamentParticipationForm" method="POST" action="{{ $relationship->relationship_type === 'admin_view' ? route('admin.platform.members.store-tournament', $relationship->dependent->id) : route('family.store-tournament', $relationship->dependent->id) }}">
@csrf
<div class="modal-body">
<div class="row g-3">
@ -2525,4 +2512,15 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
</script>
<!-- Edit Profile Modal Component -->
<x-edit-profile-modal
:user="$relationship->dependent"
:formAction="$relationship->relationship_type === 'admin_view' ? route('admin.platform.members.update', $relationship->dependent->id) : ($relationship->relationship_type === 'self' ? route('profile.update') : route('family.update', $relationship->dependent->id))"
formMethod="PUT"
:cancelUrl="null"
:uploadUrl="$relationship->relationship_type === 'admin_view' ? route('admin.platform.members.upload-picture', $relationship->dependent->id) : ($relationship->relationship_type === 'self' ? route('profile.upload-picture') : route('family.upload-picture', $relationship->dependent->id))"
:showRelationshipFields="$relationship->relationship_type !== 'admin_view' && $relationship->relationship_type !== 'self'"
:relationship="$relationship"
/>
@endsection

View File

@ -396,7 +396,7 @@
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<h6 class="dropdown-header small"><strong>{{ Auth::user()->full_name }}</strong><br><small>{{ Auth::user()->email }}</small></h6>
<div class="dropdown-divider"></div>
<a class="dropdown-item small" href="{{ url('profile') }}">
<a class="dropdown-item small" href="{{ route('member.show', Auth::id()) }}">
<i class="bi bi-person me-2"></i>My Profile
</a>
<a class="dropdown-item small" href="#">
@ -405,8 +405,8 @@
<a class="dropdown-item small" href="#">
<i class="bi bi-calendar-event me-2"></i>My Sessions
</a>
<a class="dropdown-item small" href="{{ route('family.dashboard') }}">
<i class="bi bi-people me-2"></i>My Family
<a class="dropdown-item small" href="{{ route('members.index') }}">
<i class="bi bi-people me-2"></i>Members
</a>
<a class="dropdown-item small" href="{{ route('bills.index') }}">
<i class="bi bi-receipt me-2"></i>My Bills

View File

@ -0,0 +1,276 @@
@extends('layouts.app')
@section('content')
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow-sm">
<div class="card-header bg-white">
<h4 class="mb-0">Add Family Member</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ route('family.store') }}">
@csrf
<div class="mb-3">
<label for="full_name" class="form-label">Full Name</label>
<input type="text" class="form-control @error('full_name') is-invalid @enderror" id="full_name" name="full_name" value="{{ old('full_name') }}" required>
@error('full_name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="email" class="form-label">Email Address <span class="text-muted">(Optional for children)</span></label>
<input type="email" class="form-control @error('email') is-invalid @enderror" id="email" name="email" value="{{ old('email') }}">
@error('email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="mobile" class="form-label">Mobile Number</label>
<x-country-code-dropdown
name="mobile_code"
id="country_code"
:value="old('mobile_code', '+973')"
:required="false"
:error="$errors->first('mobile_code')">
<input id="mobile_number" type="tel"
class="form-control @error('mobile') is-invalid @enderror"
name="mobile"
value="{{ old('mobile') }}"
autocomplete="tel"
placeholder="Phone number">
</x-country-code-dropdown>
@error('mobile')
<div class="invalid-feedback d-block">{{ $message }}</div>
@enderror
</div>
<div class="row mb-3">
<div class="col-md-6">
<x-gender-dropdown
name="gender"
id="gender"
:value="old('gender')"
:required="true"
:error="$errors->first('gender')" />
</div>
<div class="col-md-6">
<x-birthdate-dropdown
name="birthdate"
id="birthdate"
label="Birthdate"
:value="old('birthdate')"
:required="true"
:min-age="0"
:max-age="120"
:error="$errors->first('birthdate')" />
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="blood_type" class="form-label">Blood Type</label>
<select class="form-select @error('blood_type') is-invalid @enderror" id="blood_type" name="blood_type">
<option value="">Select Blood Type</option>
<option value="A+" {{ old('blood_type') == 'A+' ? 'selected' : '' }}>A+</option>
<option value="A-" {{ old('blood_type') == 'A-' ? 'selected' : '' }}>A-</option>
<option value="B+" {{ old('blood_type') == 'B+' ? 'selected' : '' }}>B+</option>
<option value="B-" {{ old('blood_type') == 'B-' ? 'selected' : '' }}>B-</option>
<option value="AB+" {{ old('blood_type') == 'AB+' ? 'selected' : '' }}>AB+</option>
<option value="AB-" {{ old('blood_type') == 'AB-' ? 'selected' : '' }}>AB-</option>
<option value="O+" {{ old('blood_type') == 'O+' ? 'selected' : '' }}>O+</option>
<option value="O-" {{ old('blood_type') == 'O-' ? 'selected' : '' }}>O-</option>
<option value="Unknown" {{ old('blood_type') == 'Unknown' ? 'selected' : '' }}>Unknown</option>
</select>
@error('blood_type')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-md-6">
<x-nationality-dropdown
name="nationality"
id="nationality"
:value="old('nationality')"
:required="true"
:error="$errors->first('nationality')" />
</div>
</div>
<div class="mb-3">
<h5 class="form-label d-flex justify-content-between align-items-center">
Social Media Links
<button type="button" class="btn btn-outline-primary btn-sm" id="addSocialLink">
<i class="bi bi-plus"></i> Add Link
</button>
</h5>
<div id="socialLinksContainer">
@php
$existingLinks = old('social_links', []);
if (!is_array($existingLinks)) {
$existingLinks = [];
}
// Convert associative array to array of arrays for form display
$formLinks = [];
foreach ($existingLinks as $platform => $url) {
$formLinks[] = ['platform' => $platform, 'url' => $url];
}
@endphp
@foreach($formLinks as $index => $link)
<div class="social-link-row mb-3 d-flex align-items-end">
<div class="me-2 flex-grow-1">
<label class="form-label">Platform</label>
<select class="form-select platform-select" name="social_links[{{ $index }}][platform]" required>
<option value="">Select Platform</option>
<option value="facebook" {{ ($link['platform'] ?? '') == 'facebook' ? 'selected' : '' }}>Facebook</option>
<option value="twitter" {{ ($link['platform'] ?? '') == 'twitter' ? 'selected' : '' }}>Twitter/X</option>
<option value="instagram" {{ ($link['platform'] ?? '') == 'instagram' ? 'selected' : '' }}>Instagram</option>
<option value="linkedin" {{ ($link['platform'] ?? '') == 'linkedin' ? 'selected' : '' }}>LinkedIn</option>
<option value="youtube" {{ ($link['platform'] ?? '') == 'youtube' ? 'selected' : '' }}>YouTube</option>
<option value="tiktok" {{ ($link['platform'] ?? '') == 'tiktok' ? 'selected' : '' }}>TikTok</option>
<option value="snapchat" {{ ($link['platform'] ?? '') == 'snapchat' ? 'selected' : '' }}>Snapchat</option>
<option value="whatsapp" {{ ($link['platform'] ?? '') == 'whatsapp' ? 'selected' : '' }}>WhatsApp</option>
<option value="telegram" {{ ($link['platform'] ?? '') == 'telegram' ? 'selected' : '' }}>Telegram</option>
<option value="discord" {{ ($link['platform'] ?? '') == 'discord' ? 'selected' : '' }}>Discord</option>
<option value="reddit" {{ ($link['platform'] ?? '') == 'reddit' ? 'selected' : '' }}>Reddit</option>
<option value="pinterest" {{ ($link['platform'] ?? '') == 'pinterest' ? 'selected' : '' }}>Pinterest</option>
<option value="twitch" {{ ($link['platform'] ?? '') == 'twitch' ? 'selected' : '' }}>Twitch</option>
<option value="github" {{ ($link['platform'] ?? '') == 'github' ? 'selected' : '' }}>GitHub</option>
<option value="spotify" {{ ($link['platform'] ?? '') == 'spotify' ? 'selected' : '' }}>Spotify</option>
<option value="skype" {{ ($link['platform'] ?? '') == 'skype' ? 'selected' : '' }}>Skype</option>
<option value="slack" {{ ($link['platform'] ?? '') == 'slack' ? 'selected' : '' }}>Slack</option>
<option value="medium" {{ ($link['platform'] ?? '') == 'medium' ? 'selected' : '' }}>Medium</option>
<option value="vimeo" {{ ($link['platform'] ?? '') == 'vimeo' ? 'selected' : '' }}>Vimeo</option>
<option value="messenger" {{ ($link['platform'] ?? '') == 'messenger' ? 'selected' : '' }}>Messenger</option>
<option value="wechat" {{ ($link['platform'] ?? '') == 'wechat' ? 'selected' : '' }}>WeChat</option>
<option value="line" {{ ($link['platform'] ?? '') == 'line' ? 'selected' : '' }}>Line</option>
</select>
</div>
<div class="me-2 flex-grow-1">
<label class="form-label">URL</label>
<input type="url" class="form-control" name="social_links[{{ $index }}][url]" value="{{ $link['url'] ?? '' }}" placeholder="https://example.com/username" required>
</div>
<div class="mb-0">
<button type="button" class="btn btn-outline-danger btn-sm remove-social-link">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
@endforeach
</div>
</div>
<div class="mb-3">
<label for="motto" class="form-label">Personal Motto</label>
<textarea class="form-control @error('motto') is-invalid @enderror" id="motto" name="motto" rows="3" placeholder="Enter personal motto or quote...">{{ old('motto') }}</textarea>
<div class="form-text">Share a personal motto or quote that inspires them.</div>
@error('motto')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="relationship_type" class="form-label">Relationship</label>
<select class="form-select @error('relationship_type') is-invalid @enderror" id="relationship_type" name="relationship_type" required>
<option value="">Select Relationship</option>
<option value="son" {{ old('relationship_type') == 'son' ? 'selected' : '' }}>Son</option>
<option value="daughter" {{ old('relationship_type') == 'daughter' ? 'selected' : '' }}>Daughter</option>
<option value="spouse" {{ old('relationship_type') == 'spouse' ? 'selected' : '' }}>Wife</option>
<option value="sponsor" {{ old('relationship_type') == 'sponsor' ? 'selected' : '' }}>Sponsor</option>
<option value="other" {{ old('relationship_type') == 'other' ? 'selected' : '' }}>Other</option>
</select>
@error('relationship_type')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="is_billing_contact" name="is_billing_contact" value="1" {{ old('is_billing_contact') ? 'checked' : '' }}>
<label class="form-check-label" for="is_billing_contact">Is Billing Contact</label>
</div>
<div class="d-flex justify-content-between">
<a href="{{ route('family.dashboard') }}" class="btn btn-outline-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">Add Family Member</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
let socialLinkIndex = {{ count($formLinks ?? []) }};
// Add new social link row
document.getElementById('addSocialLink').addEventListener('click', function() {
addSocialLinkRow();
});
// Remove social link row
document.addEventListener('click', function(e) {
if (e.target.classList.contains('remove-social-link') || e.target.closest('.remove-social-link')) {
e.target.closest('.social-link-row').remove();
}
});
function addSocialLinkRow(platform = '', url = '') {
const container = document.getElementById('socialLinksContainer');
const row = document.createElement('div');
row.className = 'social-link-row mb-3 d-flex align-items-end';
row.innerHTML = `
<div class="me-2 flex-grow-1">
<label class="form-label">Platform</label>
<select class="form-select platform-select" name="social_links[${socialLinkIndex}][platform]" required>
<option value="">Select Platform</option>
<option value="facebook" ${platform === 'facebook' ? 'selected' : ''}>Facebook</option>
<option value="twitter" ${platform === 'twitter' ? 'selected' : ''}>Twitter/X</option>
<option value="instagram" ${platform === 'instagram' ? 'selected' : ''}>Instagram</option>
<option value="linkedin" ${platform === 'linkedin' ? 'selected' : ''}>LinkedIn</option>
<option value="youtube" ${platform === 'youtube' ? 'selected' : ''}>YouTube</option>
<option value="tiktok" ${platform === 'tiktok' ? 'selected' : ''}>TikTok</option>
<option value="snapchat" ${platform === 'snapchat' ? 'selected' : ''}>Snapchat</option>
<option value="whatsapp" ${platform === 'whatsapp' ? 'selected' : ''}>WhatsApp</option>
<option value="telegram" ${platform === 'telegram' ? 'selected' : ''}>Telegram</option>
<option value="discord" ${platform === 'discord' ? 'selected' : ''}>Discord</option>
<option value="reddit" ${platform === 'reddit' ? 'selected' : ''}>Reddit</option>
<option value="pinterest" ${platform === 'pinterest' ? 'selected' : ''}>Pinterest</option>
<option value="twitch" ${platform === 'twitch' ? 'selected' : ''}>Twitch</option>
<option value="github" ${platform === 'github' ? 'selected' : ''}>GitHub</option>
<option value="spotify" ${platform === 'spotify' ? 'selected' : ''}>Spotify</option>
<option value="skype" ${platform === 'skype' ? 'selected' : ''}>Skype</option>
<option value="slack" ${platform === 'slack' ? 'selected' : ''}>Slack</option>
<option value="medium" ${platform === 'medium' ? 'selected' : ''}>Medium</option>
<option value="vimeo" ${platform === 'vimeo' ? 'selected' : ''}>Vimeo</option>
<option value="messenger" ${platform === 'messenger' ? 'selected' : ''}>Messenger</option>
<option value="wechat" ${platform === 'wechat' ? 'selected' : ''}>WeChat</option>
<option value="line" ${platform === 'line' ? 'selected' : ''}>Line</option>
</select>
</div>
<div class="me-2 flex-grow-1">
<label class="form-label">URL</label>
<input type="url" class="form-control" name="social_links[${socialLinkIndex}][url]" value="${url}" placeholder="https://example.com/username" required>
</div>
<div class="mb-0">
<button type="button" class="btn btn-outline-danger btn-sm remove-social-link">
<i class="bi bi-trash"></i>
</button>
</div>
`;
container.appendChild(row);
socialLinkIndex++;
}
});
</script>
@endpush
@endsection

View File

@ -0,0 +1,256 @@
@extends('layouts.app')
@section('content')
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="mb-0">Family</h1>
</div>
<!-- Family Members Card Grid -->
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-4 g-4 mb-5">
<!-- Dependents Cards -->
@foreach($dependents as $relationship)
<div class="col">
<a href="{{ route('family.show', $relationship->dependent->id) }}" class="text-decoration-none">
<div class="card h-100 shadow-sm border overflow-hidden d-flex flex-column family-card">
<!-- Header with gradient background -->
<div class="p-4 pb-3" style="background: linear-gradient(135deg, {{ $relationship->dependent->gender == 'm' ? 'rgba(147, 51, 234, 0.1) 0%, rgba(147, 51, 234, 0.05) 50%' : 'rgba(214, 51, 132, 0.1) 0%, rgba(214, 51, 132, 0.05) 50%' }}, transparent 100%);">
<div class="d-flex align-items-start gap-3">
<div class="position-relative">
<div class="rounded-circle border border-4 border-white shadow" style="width: 80px; height: 80px; overflow: hidden; box-shadow: 0 0 0 2px {{ $relationship->dependent->gender == 'm' ? 'rgba(147, 51, 234, 0.3)' : 'rgba(214, 51, 132, 0.3)' }} !important;">
@if($relationship->dependent->profile_picture)
<img src="{{ asset('storage/' . $relationship->dependent->profile_picture) }}" alt="{{ $relationship->dependent->full_name }}" class="w-100 h-100" style="object-fit: cover;">
@else
<div class="w-100 h-100 d-flex align-items-center justify-content-center text-white fw-bold fs-4" style="background: linear-gradient(135deg, {{ $relationship->dependent->gender == 'm' ? '#8b5cf6 0%, #7c3aed 100%' : '#d63384 0%, #a61e4d 100%' }});">
{{ strtoupper(substr($relationship->dependent->full_name, 0, 1)) }}
</div>
@endif
</div>
</div>
<div class="flex-grow-1 min-w-0">
<h5 class="fw-bold mb-2 text-truncate">{{ $relationship->dependent->full_name }}</h5>
<div class="d-flex flex-wrap gap-2">
@php
$age = $relationship->dependent->age;
$ageGroup = 'Adult';
if ($age < 2) {
$ageGroup = 'Infant';
} elseif ($age < 4) {
$ageGroup = 'Toddler';
} elseif ($age < 6) {
$ageGroup = 'Preschooler';
} elseif ($age < 13) {
$ageGroup = 'Child';
} elseif ($age < 20) {
$ageGroup = 'Teenager';
} elseif ($age < 40) {
$ageGroup = 'Young Adult';
} elseif ($age < 60) {
$ageGroup = 'Adult';
} else {
$ageGroup = 'Senior';
}
@endphp
<span class="badge {{ $relationship->dependent->gender == 'm' ? 'bg-primary' : 'bg-danger' }}">{{ $ageGroup }}</span>
<span class="badge bg-success">Active</span>
</div>
</div>
</div>
</div>
<!-- Contact Info -->
<div class="px-4 py-3 bg-light border-top border-bottom">
<div class="d-flex align-items-center gap-2 small mb-2">
<i class="bi bi-telephone-fill {{ $relationship->dependent->gender == 'm' ? 'text-primary' : 'text-danger' }}"></i>
<span class="fw-medium text-muted">{{ $relationship->dependent->mobile_formatted ?: ($user->mobile_formatted ?: 'Not provided') }}</span>
@if(!$relationship->dependent->mobile_formatted && $user->mobile_formatted)
<span class="badge {{ $relationship->dependent->gender == 'm' ? 'bg-info' : 'bg-danger' }} {{ $relationship->dependent->gender == 'm' ? 'text-dark' : 'text-white' }} ms-auto">Guardian's</span>
@endif
</div>
@if($relationship->dependent->email)
<div class="d-flex align-items-center gap-2 small">
<i class="bi bi-envelope-fill {{ $relationship->dependent->gender == 'm' ? 'text-primary' : 'text-danger' }}"></i>
<span class="fw-medium text-muted text-truncate">{{ $relationship->dependent->email }}</span>
</div>
@elseif($user->email)
<div class="d-flex align-items-center gap-2 small">
<i class="bi bi-envelope-fill {{ $relationship->dependent->gender == 'm' ? 'text-primary' : 'text-danger' }}"></i>
<span class="fw-medium text-muted text-truncate">{{ $user->email }}</span>
<span class="badge {{ $relationship->dependent->gender == 'm' ? 'bg-info' : 'bg-danger' }} {{ $relationship->dependent->gender == 'm' ? 'text-dark' : 'text-white' }} ms-auto">Guardian's</span>
</div>
@endif
</div>
<!-- Details -->
<div class="px-4 py-3 flex-grow-1">
<div class="row g-3 mb-3">
<div class="col-6">
<div class="small text-muted text-uppercase fw-medium mb-1" style="font-size: 0.7rem; letter-spacing: 0.5px;">Gender</div>
<div class="fw-semibold text-muted text-capitalize">{{ $relationship->dependent->gender == 'm' ? 'Male' : 'Female' }}</div>
</div>
<div class="col-6">
<div class="small text-muted text-uppercase fw-medium mb-1" style="font-size: 0.7rem; letter-spacing: 0.5px;">Age</div>
<div class="fw-semibold text-muted">{{ $relationship->dependent->age }} years</div>
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-6">
<div class="small text-muted text-uppercase fw-medium mb-1" style="font-size: 0.7rem; letter-spacing: 0.5px;">Nationality</div>
<div class="fw-semibold text-muted fs-5 nationality-display" data-iso3="{{ $relationship->dependent->nationality }}">{{ $relationship->dependent->nationality }}</div>
</div>
<div class="col-6">
<div class="small text-muted text-uppercase fw-medium mb-1" style="font-size: 0.7rem; letter-spacing: 0.5px;">Horoscope</div>
<div class="fw-semibold text-muted">
@php
$horoscopeSymbols = [
'Aries' => '♈',
'Taurus' => '♉',
'Gemini' => '♊',
'Cancer' => '♋',
'Leo' => '♌',
'Virgo' => '♍',
'Libra' => '♎',
'Scorpio' => '♏',
'Sagittarius' => '♐',
'Capricorn' => '♑',
'Aquarius' => '♒',
'Pisces' => '♓'
];
$horoscope = $relationship->dependent->horoscope ?? 'N/A';
$symbol = $horoscopeSymbols[$horoscope] ?? '';
@endphp
{{ $symbol }} {{ $horoscope }}
</div>
</div>
</div>
<div class="pt-2 border-top">
<div class="d-flex justify-content-between align-items-center small mb-2">
<span class="text-muted fw-medium">Next Birthday</span>
<span class="fw-semibold text-muted">
@if($relationship->dependent->birthdate)
{{ $relationship->dependent->birthdate->copy()->year(now()->year)->isFuture()
? $relationship->dependent->birthdate->copy()->year(now()->year)->diffForHumans(['parts' => 2, 'syntax' => \Carbon\CarbonInterface::DIFF_ABSOLUTE])
: $relationship->dependent->birthdate->copy()->year(now()->year + 1)->diffForHumans(['parts' => 2, 'syntax' => \Carbon\CarbonInterface::DIFF_ABSOLUTE]) }}
@else
N/A
@endif
</span>
</div>
<div class="d-flex justify-content-between align-items-center small">
<span class="text-muted fw-medium">Member Since</span>
<span class="fw-semibold text-muted">{{ $relationship->dependent->created_at->format('d/m/Y') }}</span>
</div>
</div>
</div>
<!-- Sponsor/Guardian Info - Footer -->
<div class="px-4 py-2 {{ $relationship->dependent->gender == 'm' ? 'bg-primary' : 'bg-danger' }} bg-opacity-10 border-top">
<div class="d-flex align-items-center justify-content-center gap-2 small">
<span class="fw-medium text-white">
{{ $relationship->relationship_type === 'spouse' ? 'WIFE' : strtoupper($relationship->relationship_type) }}
</span>
</div>
</div>
</div>
</a>
</div>
@endforeach
<!-- Add New Family Member Card -->
<div class="col">
<a href="{{ route('family.create') }}" class="text-decoration-none">
<div class="card h-100 shadow-sm border-dashed add-card">
<div class="card-body text-center d-flex flex-column justify-content-center align-items-center" style="height: 100%; cursor: pointer;">
<div class="mb-3">
<i class="bi bi-plus-circle" style="font-size: 3rem;"></i>
</div>
<h5 class="card-title text-muted">Add Family Member</h5>
</div>
</div>
</a>
</div>
</div>
</div>
<style>
.border-dashed {
border-style: dashed !important;
border-width: 2px !important;
border-color: #dee2e6 !important;
}
/* Family Card Hover Effects */
.family-card {
transition: all 0.3s ease-in-out;
cursor: pointer;
}
.family-card:hover {
transform: translateY(-8px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15) !important;
}
.family-card:hover .rounded-circle {
transform: scale(1.1);
transition: transform 0.3s ease-in-out;
}
/* Remove underline from card links */
a.text-decoration-none:hover .family-card {
text-decoration: none;
}
/* Add Card Hover Effects */
.add-card {
transition: all 0.3s ease-in-out;
}
.add-card:hover {
transform: translateY(-8px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15) !important;
border-color: #0d6efd !important;
}
.add-card:hover .bi-plus-circle {
color: #0d6efd;
transition: color 0.3s ease-in-out;
}
.add-card:hover h5 {
color: #0d6efd;
transition: color 0.3s ease-in-out;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Load countries from JSON file
fetch('/data/countries.json')
.then(response => response.json())
.then(countries => {
// Convert all nationality displays from ISO3 to country name with flag
document.querySelectorAll('.nationality-display').forEach(element => {
const iso3Code = element.getAttribute('data-iso3');
if (!iso3Code) return;
const country = countries.find(c => c.iso3 === iso3Code);
if (country) {
// Get flag emoji from ISO2 code
const flagEmoji = country.iso2
.toUpperCase()
.split('')
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
.join('');
element.textContent = `${flagEmoji} ${country.iso2.toUpperCase()}`;
}
});
})
.catch(error => console.error('Error loading countries:', error));
});
</script>
@endsection

View File

@ -0,0 +1,16 @@
@extends('layouts.app')
@section('content')
<div class="container py-4">
<x-edit-profile-modal
:user="$relationship->dependent"
:formAction="$relationship->relationship_type === 'admin_view' ? route('admin.platform.members.update', $relationship->dependent->id) : route('member.update', $relationship->dependent->id)"
formMethod="PUT"
:cancelUrl="$relationship->relationship_type === 'admin_view' ? route('admin.platform.members') : route('members.index')"
:uploadUrl="$relationship->relationship_type === 'admin_view' ? route('admin.platform.members.upload-picture', $relationship->dependent->id) : route('member.upload-picture', $relationship->dependent->id)"
:showRelationshipFields="$relationship->relationship_type !== 'admin_view' && $relationship->relationship_type !== 'self'"
:relationship="$relationship"
/>
</div>
@endsection

View File

@ -0,0 +1,256 @@
@extends('layouts.app')
@section('content')
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="mb-0">Members</h1>
</div>
<!-- Family Members Card Grid -->
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-4 g-4 mb-5">
<!-- Dependents Cards -->
@foreach($dependents as $relationship)
<div class="col">
<a href="{{ route('member.show', $relationship->dependent->id) }}" class="text-decoration-none">
<div class="card h-100 shadow-sm border overflow-hidden d-flex flex-column family-card">
<!-- Header with gradient background -->
<div class="p-4 pb-3" style="background: linear-gradient(135deg, {{ $relationship->dependent->gender == 'm' ? 'rgba(147, 51, 234, 0.1) 0%, rgba(147, 51, 234, 0.05) 50%' : 'rgba(214, 51, 132, 0.1) 0%, rgba(214, 51, 132, 0.05) 50%' }}, transparent 100%);">
<div class="d-flex align-items-start gap-3">
<div class="position-relative">
<div class="rounded-circle border border-4 border-white shadow" style="width: 80px; height: 80px; overflow: hidden; box-shadow: 0 0 0 2px {{ $relationship->dependent->gender == 'm' ? 'rgba(147, 51, 234, 0.3)' : 'rgba(214, 51, 132, 0.3)' }} !important;">
@if($relationship->dependent->profile_picture)
<img src="{{ asset('storage/' . $relationship->dependent->profile_picture) }}" alt="{{ $relationship->dependent->full_name }}" class="w-100 h-100" style="object-fit: cover;">
@else
<div class="w-100 h-100 d-flex align-items-center justify-content-center text-white fw-bold fs-4" style="background: linear-gradient(135deg, {{ $relationship->dependent->gender == 'm' ? '#8b5cf6 0%, #7c3aed 100%' : '#d63384 0%, #a61e4d 100%' }});">
{{ strtoupper(substr($relationship->dependent->full_name, 0, 1)) }}
</div>
@endif
</div>
</div>
<div class="flex-grow-1 min-w-0">
<h5 class="fw-bold mb-2 text-truncate">{{ $relationship->dependent->full_name }}</h5>
<div class="d-flex flex-wrap gap-2">
@php
$age = $relationship->dependent->age;
$ageGroup = 'Adult';
if ($age < 2) {
$ageGroup = 'Infant';
} elseif ($age < 4) {
$ageGroup = 'Toddler';
} elseif ($age < 6) {
$ageGroup = 'Preschooler';
} elseif ($age < 13) {
$ageGroup = 'Child';
} elseif ($age < 20) {
$ageGroup = 'Teenager';
} elseif ($age < 40) {
$ageGroup = 'Young Adult';
} elseif ($age < 60) {
$ageGroup = 'Adult';
} else {
$ageGroup = 'Senior';
}
@endphp
<span class="badge {{ $relationship->dependent->gender == 'm' ? 'bg-primary' : 'bg-danger' }}">{{ $ageGroup }}</span>
<span class="badge bg-success">Active</span>
</div>
</div>
</div>
</div>
<!-- Contact Info -->
<div class="px-4 py-3 bg-light border-top border-bottom">
<div class="d-flex align-items-center gap-2 small mb-2">
<i class="bi bi-telephone-fill {{ $relationship->dependent->gender == 'm' ? 'text-primary' : 'text-danger' }}"></i>
<span class="fw-medium text-muted">{{ $relationship->dependent->mobile_formatted ?: ($user->mobile_formatted ?: 'Not provided') }}</span>
@if(!$relationship->dependent->mobile_formatted && $user->mobile_formatted)
<span class="badge {{ $relationship->dependent->gender == 'm' ? 'bg-info' : 'bg-danger' }} {{ $relationship->dependent->gender == 'm' ? 'text-dark' : 'text-white' }} ms-auto">Guardian's</span>
@endif
</div>
@if($relationship->dependent->email)
<div class="d-flex align-items-center gap-2 small">
<i class="bi bi-envelope-fill {{ $relationship->dependent->gender == 'm' ? 'text-primary' : 'text-danger' }}"></i>
<span class="fw-medium text-muted text-truncate">{{ $relationship->dependent->email }}</span>
</div>
@elseif($user->email)
<div class="d-flex align-items-center gap-2 small">
<i class="bi bi-envelope-fill {{ $relationship->dependent->gender == 'm' ? 'text-primary' : 'text-danger' }}"></i>
<span class="fw-medium text-muted text-truncate">{{ $user->email }}</span>
<span class="badge {{ $relationship->dependent->gender == 'm' ? 'bg-info' : 'bg-danger' }} {{ $relationship->dependent->gender == 'm' ? 'text-dark' : 'text-white' }} ms-auto">Guardian's</span>
</div>
@endif
</div>
<!-- Details -->
<div class="px-4 py-3 flex-grow-1">
<div class="row g-3 mb-3">
<div class="col-6">
<div class="small text-muted text-uppercase fw-medium mb-1" style="font-size: 0.7rem; letter-spacing: 0.5px;">Gender</div>
<div class="fw-semibold text-muted text-capitalize">{{ $relationship->dependent->gender == 'm' ? 'Male' : 'Female' }}</div>
</div>
<div class="col-6">
<div class="small text-muted text-uppercase fw-medium mb-1" style="font-size: 0.7rem; letter-spacing: 0.5px;">Age</div>
<div class="fw-semibold text-muted">{{ $relationship->dependent->age }} years</div>
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-6">
<div class="small text-muted text-uppercase fw-medium mb-1" style="font-size: 0.7rem; letter-spacing: 0.5px;">Nationality</div>
<div class="fw-semibold text-muted fs-5 nationality-display" data-iso3="{{ $relationship->dependent->nationality }}">{{ $relationship->dependent->nationality }}</div>
</div>
<div class="col-6">
<div class="small text-muted text-uppercase fw-medium mb-1" style="font-size: 0.7rem; letter-spacing: 0.5px;">Horoscope</div>
<div class="fw-semibold text-muted">
@php
$horoscopeSymbols = [
'Aries' => '♈',
'Taurus' => '♉',
'Gemini' => '♊',
'Cancer' => '♋',
'Leo' => '♌',
'Virgo' => '♍',
'Libra' => '♎',
'Scorpio' => '♏',
'Sagittarius' => '♐',
'Capricorn' => '♑',
'Aquarius' => '♒',
'Pisces' => '♓'
];
$horoscope = $relationship->dependent->horoscope ?? 'N/A';
$symbol = $horoscopeSymbols[$horoscope] ?? '';
@endphp
{{ $symbol }} {{ $horoscope }}
</div>
</div>
</div>
<div class="pt-2 border-top">
<div class="d-flex justify-content-between align-items-center small mb-2">
<span class="text-muted fw-medium">Next Birthday</span>
<span class="fw-semibold text-muted">
@if($relationship->dependent->birthdate)
{{ $relationship->dependent->birthdate->copy()->year(now()->year)->isFuture()
? $relationship->dependent->birthdate->copy()->year(now()->year)->diffForHumans(['parts' => 2, 'syntax' => \Carbon\CarbonInterface::DIFF_ABSOLUTE])
: $relationship->dependent->birthdate->copy()->year(now()->year + 1)->diffForHumans(['parts' => 2, 'syntax' => \Carbon\CarbonInterface::DIFF_ABSOLUTE]) }}
@else
N/A
@endif
</span>
</div>
<div class="d-flex justify-content-between align-items-center small">
<span class="text-muted fw-medium">Member Since</span>
<span class="fw-semibold text-muted">{{ $relationship->dependent->created_at->format('d/m/Y') }}</span>
</div>
</div>
</div>
<!-- Sponsor/Guardian Info - Footer -->
<div class="px-4 py-2 {{ $relationship->dependent->gender == 'm' ? 'bg-primary' : 'bg-danger' }} bg-opacity-10 border-top">
<div class="d-flex align-items-center justify-content-center gap-2 small">
<span class="fw-medium text-white">
{{ $relationship->relationship_type === 'spouse' ? 'WIFE' : strtoupper($relationship->relationship_type) }}
</span>
</div>
</div>
</div>
</a>
</div>
@endforeach
<!-- Add New Family Member Card -->
<div class="col">
<a href="{{ route('members.create') }}" class="text-decoration-none">
<div class="card h-100 shadow-sm border-dashed add-card">
<div class="card-body text-center d-flex flex-column justify-content-center align-items-center" style="height: 100%; cursor: pointer;">
<div class="mb-3">
<i class="bi bi-plus-circle" style="font-size: 3rem;"></i>
</div>
<h5 class="card-title text-muted">Add Member</h5>
</div>
</div>
</a>
</div>
</div>
</div>
<style>
.border-dashed {
border-style: dashed !important;
border-width: 2px !important;
border-color: #dee2e6 !important;
}
/* Family Card Hover Effects */
.family-card {
transition: all 0.3s ease-in-out;
cursor: pointer;
}
.family-card:hover {
transform: translateY(-8px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15) !important;
}
.family-card:hover .rounded-circle {
transform: scale(1.1);
transition: transform 0.3s ease-in-out;
}
/* Remove underline from card links */
a.text-decoration-none:hover .family-card {
text-decoration: none;
}
/* Add Card Hover Effects */
.add-card {
transition: all 0.3s ease-in-out;
}
.add-card:hover {
transform: translateY(-8px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15) !important;
border-color: #0d6efd !important;
}
.add-card:hover .bi-plus-circle {
color: #0d6efd;
transition: color 0.3s ease-in-out;
}
.add-card:hover h5 {
color: #0d6efd;
transition: color 0.3s ease-in-out;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Load countries from JSON file
fetch('/data/countries.json')
.then(response => response.json())
.then(countries => {
// Convert all nationality displays from ISO3 to country name with flag
document.querySelectorAll('.nationality-display').forEach(element => {
const iso3Code = element.getAttribute('data-iso3');
if (!iso3Code) return;
const country = countries.find(c => c.iso3 === iso3Code);
if (country) {
// Get flag emoji from ISO2 code
const flagEmoji = country.iso2
.toUpperCase()
.split('')
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
.join('');
element.textContent = `${flagEmoji} ${country.iso2.toUpperCase()}`;
}
});
})
.catch(error => console.error('Error loading countries:', error));
});
</script>
@endsection

View File

@ -0,0 +1,916 @@
@php
// Helper function to calculate age at a specific date with detailed format
function calculateAgeAtDate($birthdate, $date) {
if (!$birthdate || !$date) return null;
$birth = \Carbon\Carbon::parse($birthdate);
$targetDate = \Carbon\Carbon::parse($date);
$diff = $birth->diff($targetDate);
$parts = [];
if ($diff->y > 0) $parts[] = $diff->y . ' year' . ($diff->y > 1 ? 's' : '');
if ($diff->m > 0) $parts[] = $diff->m . ' month' . ($diff->m > 1 ? 's' : '');
if ($diff->d > 0) $parts[] = $diff->d . ' day' . ($diff->d > 1 ? 's' : '');
return implode(' ', $parts) ?: 'Same day';
}
@endphp
<div class="card shadow-sm border-0">
<div class="card-body p-4">
<!-- Header with Filter -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h5 class="fw-bold mb-1"><i class="bi bi-diagram-3 me-2"></i>Club Affiliations & Skills Journey</h5>
<p class="text-muted small mb-0">Complete history of club memberships, skills acquired, and instructors</p>
</div>
<div class="d-flex gap-2">
<select class="form-select form-select-sm" id="skillFilter" style="width: 200px;">
<option value="all">All Skills</option>
@foreach($allSkills ?? [] as $skill)
<option value="{{ $skill }}">{{ $skill }}</option>
@endforeach
</select>
<button class="btn btn-sm btn-outline-secondary" id="resetFilters">
<i class="bi bi-arrow-clockwise"></i> Reset
</button>
</div>
</div>
@if($clubAffiliations->count() > 0)
<!-- Summary Stats -->
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="card shadow-sm h-100" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none;">
<div class="card-body text-center text-white p-3">
<i class="bi bi-building display-5 mb-2"></i>
<h3 class="fw-bold mb-1">{{ $totalAffiliations }}</h3>
<small class="opacity-75">Total Clubs</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm h-100" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border: none;">
<div class="card-body text-center text-white p-3">
<i class="bi bi-star-fill display-5 mb-2"></i>
<h3 class="fw-bold mb-1">{{ $distinctSkills }}</h3>
<small class="opacity-75">Unique Skills</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm h-100" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); border: none;">
<div class="card-body text-center text-white p-3">
<i class="bi bi-calendar-check display-5 mb-2"></i>
<h3 class="fw-bold mb-1">{{ floor($totalMembershipDuration / 12) }}y {{ $totalMembershipDuration % 12 }}m</h3>
<small class="opacity-75">Total Training</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm h-100" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); border: none;">
<div class="card-body text-center text-white p-3">
<i class="bi bi-people-fill display-5 mb-2"></i>
<h3 class="fw-bold mb-1">{{ $totalInstructors ?? 0 }}</h3>
<small class="opacity-75">Instructors</small>
</div>
</div>
</div>
</div>
<!-- Timeline -->
<div class="row">
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-header" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none;">
<h6 class="card-title mb-0 text-white">
<i class="bi bi-clock-history me-2"></i>Membership Timeline
</h6>
</div>
<div class="card-body p-4" style="max-height: 800px; overflow-y: auto;">
<div class="timeline-enhanced">
@foreach($clubAffiliations as $index => $affiliation)
@php
$ageAtStart = calculateAgeAtDate($user->birthdate, $affiliation->start_date);
$ageAtEnd = $affiliation->end_date ? calculateAgeAtDate($user->birthdate, $affiliation->end_date) : null;
$isOngoing = !$affiliation->end_date;
// Get all skills for this affiliation
$affiliationSkills = $affiliation->skillAcquisitions ?? collect();
$skillNames = $affiliationSkills->pluck('skill_name')->unique()->implode(',');
@endphp
<div class="timeline-item-enhanced mb-4" data-affiliation-id="{{ $affiliation->id }}" data-skills="{{ $skillNames }}">
<!-- Timeline Marker -->
<div class="timeline-marker-enhanced {{ $isOngoing ? 'pulse' : '' }}"></div>
<!-- Affiliation Card -->
<div class="affiliation-card-enhanced card border-0 shadow-sm">
<!-- Card Header with Gradient -->
<div class="card-header border-0 p-3" style="background: linear-gradient(135deg, {{ $index % 4 == 0 ? '#667eea 0%, #764ba2' : ($index % 4 == 1 ? '#f093fb 0%, #f5576c' : ($index % 4 == 2 ? '#4facfe 0%, #00f2fe' : '#fa709a 0%, #fee140')) }} 100%);">
<div class="d-flex align-items-center">
@if($affiliation->logo)
<img src="{{ asset('storage/' . $affiliation->logo) }}" alt="{{ $affiliation->club_name }}" class="rounded-circle me-3" style="width: 50px; height: 50px; object-fit: cover; border: 3px solid white;">
@else
<div class="rounded-circle bg-white d-flex align-items-center justify-content-center me-3" style="width: 50px; height: 50px;">
<i class="bi bi-building" style="font-size: 1.5rem; color: #667eea;"></i>
</div>
@endif
<div class="flex-grow-1 text-white">
<h5 class="mb-1 fw-bold">{{ $affiliation->club_name }}</h5>
<div class="d-flex gap-3 flex-wrap">
@if($affiliation->start_date)
<small class="opacity-90">
<i class="bi bi-calendar-event me-1"></i>{{ $affiliation->start_date->format('M Y') }} - {{ $isOngoing ? 'Present' : ($affiliation->end_date ? $affiliation->end_date->format('M Y') : 'N/A') }}
</small>
@endif
@if($affiliation->formatted_duration)
<small class="opacity-90">
<i class="bi bi-hourglass-split me-1"></i>{{ $affiliation->formatted_duration }}
</small>
@endif
@if($ageAtStart)
<small class="opacity-90">
<i class="bi bi-person me-1"></i>Age: {{ $ageAtStart }}{{ $ageAtEnd && $ageAtEnd != $ageAtStart ? " to $ageAtEnd" : '' }}
</small>
@endif
</div>
</div>
@if($isOngoing)
<span class="badge bg-success">
<i class="bi bi-circle-fill me-1" style="font-size: 0.5rem;"></i>Active
</span>
@endif
</div>
</div>
<!-- Card Body -->
<div class="card-body p-3">
@if($affiliation->location)
<div class="mb-3">
<i class="bi bi-geo-alt text-primary me-2"></i>
<span class="text-muted">{{ $affiliation->location }}</span>
</div>
@endif
<!-- Skills Acquired as Badges -->
@if($affiliationSkills->count() > 0)
<div class="mb-3">
<h6 class="fw-bold mb-2">
<i class="bi bi-star-fill me-2 text-warning"></i>Skills Acquired ({{ $affiliationSkills->count() }})
</h6>
<div class="d-flex gap-2 flex-wrap">
@foreach($affiliationSkills as $skill)
<span class="badge skill-badge bg-{{ $skill->proficiency_level == 'expert' ? 'danger' : ($skill->proficiency_level == 'advanced' ? 'warning' : ($skill->proficiency_level == 'intermediate' ? 'info' : 'secondary')) }}"
data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-html="true"
title="<strong>{{ $skill->skill_name }}</strong><br>
Proficiency: {{ ucfirst($skill->proficiency_level) }}<br>
Duration: {{ $skill->formatted_duration }}<br>
@if($skill->instructor)Instructor: {{ $skill->instructor->user->full_name ?? 'Unknown' }}<br>@endif
@if($skill->start_date)Started: {{ $skill->start_date->format('M Y') }}@endif">
<i class="bi bi-star-fill me-1"></i>{{ $skill->skill_name }}
<span class="badge bg-white text-dark ms-1" style="font-size: 0.65rem;">{{ ucfirst($skill->proficiency_level) }}</span>
</span>
@endforeach
</div>
</div>
@endif
<!-- Training Packages -->
@if($affiliation->subscriptions && $affiliation->subscriptions->count() > 0)
<div class="mb-3">
<h6 class="fw-bold mb-2">
<i class="bi bi-box-seam me-2 text-primary"></i>Training Packages ({{ $affiliation->subscriptions->count() }})
</h6>
<div class="d-flex gap-2 flex-wrap">
@foreach($affiliation->subscriptions as $subIndex => $subscription)
@if($subscription->package)
<button type="button" class="btn btn-sm btn-outline-primary package-card-btn"
data-bs-toggle="modal"
data-bs-target="#packageModal_{{ $affiliation->id }}_{{ $subscription->id }}">
<i class="bi bi-box me-1"></i>{{ $subscription->package->name }}
</button>
@endif
@endforeach
</div>
</div>
@endif
<!-- Instructors -->
@php
$instructors = $affiliationSkills->pluck('instructor')->filter()->unique('id');
@endphp
@if($instructors->count() > 0)
<div class="mb-2">
<h6 class="fw-bold mb-2">
<i class="bi bi-people-fill me-2 text-success"></i>Instructors ({{ $instructors->count() }})
</h6>
<div class="d-flex gap-2 flex-wrap">
@foreach($instructors as $instructor)
<div class="instructor-badge" role="button"
data-bs-toggle="modal"
data-bs-target="#instructorModal_{{ $instructor->id }}">
<div class="d-flex align-items-center gap-2 p-2 bg-light rounded">
<div class="rounded-circle bg-success text-white d-flex align-items-center justify-content-center" style="width: 32px; height: 32px; font-size: 0.8rem;">
{{ strtoupper(substr($instructor->user->full_name ?? 'I', 0, 1)) }}
</div>
<div>
<div class="fw-semibold small">{{ $instructor->user->full_name ?? 'Unknown' }}</div>
<div class="text-muted" style="font-size: 0.7rem;">{{ $instructor->role ?? 'Instructor' }}</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
@endif
</div>
</div>
</div>
@endforeach
</div>
<!-- Instructor Modals -->
@php
$allInstructors = collect();
foreach($clubAffiliations as $aff) {
$affInstructors = $aff->skillAcquisitions->pluck('instructor')->filter()->unique('id');
$allInstructors = $allInstructors->merge($affInstructors);
}
$allInstructors = $allInstructors->unique('id');
@endphp
@foreach($allInstructors as $instructor)
<div class="modal fade" id="instructorModal_{{ $instructor->id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" style="max-width: 600px;">
<div class="modal-content">
<div class="modal-header" style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);">
<h5 class="modal-title text-white">
<i class="bi bi-person-badge me-2"></i>Instructor Profile
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" style="max-height: 70vh; overflow-y: auto;">
<div class="text-center mb-4">
<!-- Profile Picture -->
<div class="mb-3">
@if($instructor->user->profile_picture)
<img src="{{ asset('storage/' . $instructor->user->profile_picture) }}"
alt="{{ $instructor->user->full_name }}"
class="rounded-circle"
style="width: 100px; height: 100px; object-fit: cover; border: 4px solid #11998e;">
@else
<div class="rounded-circle mx-auto d-flex align-items-center justify-content-center text-white"
style="width: 100px; height: 100px; font-size: 2.5rem; background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);">
{{ strtoupper(substr($instructor->user->full_name ?? 'I', 0, 1)) }}
</div>
@endif
</div>
<!-- Name & Role -->
<h5 class="fw-bold mb-1">{{ $instructor->user->full_name ?? 'Unknown Instructor' }}</h5>
<p class="text-muted mb-2">{{ $instructor->role ?? 'Instructor' }}</p>
<!-- Average Rating -->
@php
$avgRating = $instructor->reviews()->avg('rating') ?? 0;
$reviewCount = $instructor->reviews()->count();
@endphp
<div class="mb-3">
<div class="d-flex justify-content-center align-items-center gap-2">
<div class="stars-display">
@for($i = 1; $i <= 5; $i++)
<i class="bi bi-star{{ $i <= round($avgRating) ? '-fill' : '' }} text-warning"></i>
@endfor
</div>
<span class="text-muted small">({{ number_format($avgRating, 1) }} / {{ $reviewCount }} {{ $reviewCount == 1 ? 'review' : 'reviews' }})</span>
</div>
</div>
</div>
<!-- Quick Stats -->
<div class="row g-2 mb-3">
@php
$instructorSkills = \App\Models\SkillAcquisition::where('instructor_id', $instructor->id)->get();
$studentsCount = $instructorSkills->pluck('clubAffiliation.member_id')->unique()->count();
$skillsTaught = $instructorSkills->pluck('skill_name')->unique();
@endphp
<div class="col-6">
<div class="card bg-light border-0">
<div class="card-body p-2">
<div class="h4 mb-0 text-primary">{{ $studentsCount }}</div>
<small class="text-muted">Students</small>
</div>
</div>
</div>
<div class="col-6">
<div class="card bg-light border-0">
<div class="card-body p-2">
<div class="h4 mb-0 text-success">{{ $skillsTaught->count() }}</div>
<small class="text-muted">Skills</small>
</div>
</div>
</div>
</div>
<!-- Skills Taught -->
@if($skillsTaught->count() > 0)
<div class="mb-3 text-start">
<label class="text-muted small fw-semibold mb-2">Specializes In:</label>
<div class="d-flex gap-1 flex-wrap">
@foreach($skillsTaught as $skill)
<span class="badge bg-success">{{ $skill }}</span>
@endforeach
</div>
</div>
@endif
<!-- Contact Info -->
@if($instructor->user->email)
<div class="mb-2 text-start">
<small class="text-muted">
<i class="bi bi-envelope me-1"></i>{{ $instructor->user->email }}
</small>
</div>
@endif
@if($instructor->user->mobile)
<div class="mb-3 text-start">
<small class="text-muted">
<i class="bi bi-phone me-1"></i>{{ $instructor->user->mobile }}
</small>
</div>
@endif
<!-- Reviews Section -->
<div class="mt-4">
<h6 class="fw-bold mb-3">
<i class="bi bi-chat-left-text me-2"></i>Reviews
</h6>
<!-- Add/Edit Review Form -->
@php
$userReview = $instructor->reviews()->where('reviewer_user_id', auth()->id())->first();
@endphp
<div class="card bg-light mb-3" id="reviewForm_{{ $instructor->id }}">
<div class="card-body">
<form class="instructor-review-form" data-instructor-id="{{ $instructor->id }}" data-review-id="{{ $userReview->id ?? '' }}">
@csrf
<div class="mb-3">
<label class="form-label small fw-semibold">Your Rating</label>
<div class="star-rating" data-rating="{{ $userReview->rating ?? 0 }}">
@for($i = 1; $i <= 5; $i++)
<i class="bi bi-star{{ $userReview && $i <= $userReview->rating ? '-fill' : '' }} star-input" data-value="{{ $i }}"></i>
@endfor
</div>
<input type="hidden" name="rating" value="{{ $userReview->rating ?? 0 }}" required>
</div>
<div class="mb-3">
<label class="form-label small fw-semibold">Your Review</label>
<textarea name="comment" class="form-control form-control-sm" rows="3" placeholder="Share your experience...">{{ $userReview->comment ?? '' }}</textarea>
</div>
<button type="submit" class="btn btn-success btn-sm w-100">
<i class="bi bi-{{ $userReview ? 'pencil' : 'plus-circle' }} me-1"></i>
{{ $userReview ? 'Update Review' : 'Submit Review' }}
</button>
</form>
</div>
</div>
<!-- Reviews List -->
<div class="reviews-list" id="reviewsList_{{ $instructor->id }}">
@foreach($instructor->reviews()->with('reviewer')->latest()->get() as $review)
<div class="card mb-2">
<div class="card-body p-3">
<div class="d-flex align-items-start mb-2">
<div class="flex-grow-1">
<div class="fw-semibold small">{{ $review->reviewer->full_name }}</div>
<div class="stars-display small">
@for($i = 1; $i <= 5; $i++)
<i class="bi bi-star{{ $i <= $review->rating ? '-fill' : '' }} text-warning"></i>
@endfor
</div>
</div>
<small class="text-muted">
{{ $review->wasUpdated() ? 'Updated ' : '' }}{{ $review->wasUpdated() ? $review->updated_at->diffForHumans() : $review->reviewed_at->diffForHumans() }}
</small>
</div>
@if($review->comment)
<p class="mb-0 small text-muted">{{ $review->comment }}</p>
@endif
</div>
</div>
@endforeach
</div>
</div>
</div>
<div class="modal-footer">
<a href="{{ route('family.show', $instructor->user_id) }}" class="btn btn-primary btn-sm">
<i class="bi bi-person-lines-fill me-1"></i>View Full Profile
</a>
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
@endforeach
<!-- Package Modals (Outside Timeline Loop) -->
@foreach($clubAffiliations as $affiliation)
@foreach($affiliation->subscriptions as $subIndex => $subscription)
@if($subscription->package)
<div class="modal fade" id="packageModal_{{ $affiliation->id }}_{{ $subscription->id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<h5 class="modal-title text-white">
<i class="bi bi-box-seam me-2"></i>{{ $subscription->package->name }}
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="text-muted small fw-semibold">Subscription Period</label>
<div>
<i class="bi bi-calendar-range me-2 text-primary"></i>
{{ $subscription->start_date ? $subscription->start_date->format('M d, Y') : 'N/A' }} - {{ $subscription->end_date ? $subscription->end_date->format('M d, Y') : 'N/A' }}
</div>
@php
$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
<small class="text-muted">
<i class="bi bi-hourglass-split me-1"></i>Duration: {{ $durationText }}
</small>
</div>
@if($subscription->package->description)
<div class="mb-3">
<label class="text-muted small fw-semibold">Description</label>
<p class="mb-0">{{ $subscription->package->description }}</p>
</div>
@endif
@if($subscription->package->price)
<div class="mb-3">
<label class="text-muted small fw-semibold">Price</label>
<div class="h5 mb-0 text-success">
<i class="bi bi-currency-dollar"></i>{{ number_format($subscription->package->price, 2) }}
</div>
</div>
@endif
@if($subscription->package->packageActivities && $subscription->package->packageActivities->count() > 0)
<div class="mb-3">
<label class="text-muted small fw-semibold">Activities & Skills Included</label>
<div class="list-group">
@foreach($subscription->package->packageActivities as $pkgActivity)
@if($pkgActivity->activity)
<div class="list-group-item">
<div class="d-flex align-items-start mb-2">
<i class="bi bi-check-circle-fill text-success me-2 mt-1"></i>
<div class="flex-grow-1">
<div class="fw-semibold">{{ $pkgActivity->activity->name }}</div>
@if($pkgActivity->activity->description)
<small class="text-muted d-block mb-2">{{ $pkgActivity->activity->description }}</small>
@endif
@php
// Get skills taught in this activity
$activitySkills = \App\Models\SkillAcquisition::where('activity_id', $pkgActivity->activity_id)
->where('club_affiliation_id', $affiliation->id)
->get();
@endphp
@if($activitySkills->count() > 0)
<div class="mb-2">
<small class="text-muted d-block mb-1">Skills Practiced:</small>
<div class="d-flex gap-1 flex-wrap">
@foreach($activitySkills as $actSkill)
<span class="badge bg-{{ $actSkill->proficiency_level == 'expert' ? 'danger' : ($actSkill->proficiency_level == 'advanced' ? 'warning' : ($actSkill->proficiency_level == 'intermediate' ? 'info' : 'secondary')) }}" style="font-size: 0.7rem;">
<i class="bi bi-star-fill me-1"></i>{{ $actSkill->skill_name }}
</span>
@endforeach
</div>
</div>
@endif
</div>
@if($pkgActivity->instructor && $pkgActivity->instructor->user)
<div class="text-end">
<small class="text-muted">
<i class="bi bi-person-badge"></i>
{{ $pkgActivity->instructor->user->full_name }}
</small>
</div>
@endif
</div>
</div>
@endif
@endforeach
</div>
</div>
@endif
@php
// Check if this package was subscribed to multiple times
$samePackageSubscriptions = $affiliation->subscriptions
->where('package_id', $subscription->package_id)
->where('id', '!=', $subscription->id);
@endphp
@if($samePackageSubscriptions->count() > 0)
<div class="mb-3">
<label class="text-muted small fw-semibold">
<i class="bi bi-arrow-repeat me-1"></i>Other Subscriptions to This Package
</label>
<div class="alert alert-info mb-0" style="font-size: 0.85rem;">
<div class="fw-semibold mb-1">You subscribed to this package {{ $samePackageSubscriptions->count() + 1 }} times:</div>
<ul class="mb-0 ps-3">
<li class="text-primary fw-semibold">
{{ $subscription->start_date ? $subscription->start_date->format('M d, Y') : 'N/A' }} - {{ $subscription->end_date ? $subscription->end_date->format('M d, Y') : 'N/A' }} (Current)
</li>
@foreach($samePackageSubscriptions as $otherSub)
<li>
{{ $otherSub->start_date ? $otherSub->start_date->format('M d, Y') : 'N/A' }} - {{ $otherSub->end_date ? $otherSub->end_date->format('M d, Y') : 'N/A' }}
@php
$gap = 0;
if ($subscription->start_date && $otherSub->start_date) {
$gap = $subscription->start_date->diffInMonths($otherSub->start_date);
}
@endphp
@if($gap > 0)
<small class="text-muted">({{ abs($gap) }} months {{ $subscription->start_date->gt($otherSub->start_date) ? 'before' : 'after' }} current)</small>
@endif
</li>
@endforeach
</ul>
</div>
</div>
@endif
<div class="mb-0">
<label class="text-muted small fw-semibold">Status</label>
<div>
<span class="badge bg-{{ $subscription->status == 'active' ? 'success' : 'secondary' }}">
{{ ucfirst($subscription->status) }}
</span>
<span class="badge bg-{{ $subscription->payment_status == 'paid' ? 'success' : 'warning' }} ms-2">
Payment: {{ ucfirst($subscription->payment_status) }}
</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
@endif
@endforeach
@endforeach
</div>
</div>
</div>
</div>
@else
<div class="text-center py-5">
<i class="bi bi-diagram-3 text-muted" style="font-size: 3rem;"></i>
<h5 class="text-muted mt-3 mb-2">No Affiliations Yet</h5>
<p class="text-muted mb-0">Club affiliations and skills will appear here once added</p>
</div>
@endif
</div>
</div>
<style>
/* Enhanced Timeline Styles */
.timeline-enhanced {
position: relative;
padding-left: 40px;
}
.timeline-enhanced::before {
content: '';
position: absolute;
left: 20px;
top: 0;
bottom: 0;
width: 3px;
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
}
.timeline-item-enhanced {
position: relative;
animation: fadeInUp 0.5s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.timeline-marker-enhanced {
position: absolute;
left: -28px;
top: 25px;
width: 16px;
height: 16px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: 3px solid #fff;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
z-index: 1;
}
.timeline-marker-enhanced.pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% {
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
}
50% {
box-shadow: 0 0 0 8px rgba(102, 126, 234, 0.4);
}
}
.affiliation-card-enhanced {
transition: all 0.3s ease;
margin-left: 10px;
}
.affiliation-card-enhanced:hover {
transform: translateX(5px);
box-shadow: 0 8px 16px rgba(0,0,0,0.15) !important;
}
.skill-badge {
padding: 0.5rem 0.75rem;
font-size: 0.85rem;
font-weight: 500;
transition: all 0.2s ease;
cursor: pointer;
}
.skill-badge:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.package-card-btn {
transition: all 0.2s ease;
}
.package-card-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.modal-dialog {
max-width: 600px;
}
.modal-body {
max-height: 60vh;
overflow-y: auto;
}
.instructor-badge {
transition: all 0.2s ease;
cursor: pointer;
}
.instructor-badge:hover {
transform: scale(1.05);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.instructor-badge:active {
transform: scale(0.98);
}
/* Star Rating Styles */
.star-rating {
font-size: 1.5rem;
cursor: pointer;
}
.star-rating .star-input {
color: #ddd;
transition: color 0.2s ease;
}
.star-rating .star-input:hover,
.star-rating .star-input.active {
color: #ffc107;
}
.star-rating .bi-star-fill {
color: #ffc107;
}
.stars-display {
font-size: 1rem;
}
.stars-display .bi-star-fill {
color: #ffc107;
}
.stars-display .bi-star {
color: #ddd;
}
/* Filter transition */
.timeline-item-enhanced.filtered-out {
display: none;
animation: fadeOut 0.3s ease-out;
}
@keyframes fadeOut {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-20px);
}
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const skillFilter = document.getElementById('skillFilter');
const resetButton = document.getElementById('resetFilters');
const timelineItems = document.querySelectorAll('.timeline-item-enhanced');
// Initialize Bootstrap tooltips
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// Star Rating Functionality
document.querySelectorAll('.star-rating').forEach(ratingContainer => {
const stars = ratingContainer.querySelectorAll('.star-input');
const ratingInput = ratingContainer.parentElement.querySelector('input[name="rating"]');
stars.forEach((star, index) => {
star.addEventListener('click', function() {
const value = this.getAttribute('data-value');
ratingInput.value = value;
// Update star display
stars.forEach((s, i) => {
if (i < value) {
s.classList.remove('bi-star');
s.classList.add('bi-star-fill');
} else {
s.classList.remove('bi-star-fill');
s.classList.add('bi-star');
}
});
});
star.addEventListener('mouseenter', function() {
const value = this.getAttribute('data-value');
stars.forEach((s, i) => {
if (i < value) {
s.classList.add('active');
} else {
s.classList.remove('active');
}
});
});
});
ratingContainer.addEventListener('mouseleave', function() {
stars.forEach(s => s.classList.remove('active'));
});
});
// Review Form Submission
document.querySelectorAll('.instructor-review-form').forEach(form => {
form.addEventListener('submit', function(e) {
e.preventDefault();
const instructorId = this.getAttribute('data-instructor-id');
const reviewId = this.getAttribute('data-review-id');
const formData = new FormData(this);
const data = {
rating: formData.get('rating'),
comment: formData.get('comment')
};
// Validate rating
if (!data.rating || data.rating == 0) {
alert('Please select a rating');
return;
}
const url = reviewId
? `/instructor/reviews/${reviewId}`
: `/instructor/${instructorId}/reviews`;
const method = reviewId ? 'PUT' : 'POST';
fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
// Show success message
showAlert(result.message, 'success');
// Reload the page to show updated reviews
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
showAlert(result.message || 'Error submitting review', 'danger');
}
})
.catch(error => {
console.error('Error:', error);
showAlert('Error submitting review. Please try again.', 'danger');
});
});
});
function showAlert(message, type) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
document.body.appendChild(alertDiv);
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
if (skillFilter) {
skillFilter.addEventListener('change', function() {
const selectedSkill = this.value;
timelineItems.forEach(item => {
const itemSkills = item.getAttribute('data-skills');
if (selectedSkill === 'all') {
item.classList.remove('filtered-out');
item.style.display = '';
} else {
if (itemSkills && itemSkills.includes(selectedSkill)) {
item.classList.remove('filtered-out');
item.style.display = '';
} else {
item.classList.add('filtered-out');
setTimeout(() => {
item.style.display = 'none';
}, 300);
}
}
});
});
}
if (resetButton) {
resetButton.addEventListener('click', function() {
if (skillFilter) {
skillFilter.value = 'all';
skillFilter.dispatchEvent(new Event('change'));
}
});
}
});
</script>

View File

@ -0,0 +1,14 @@
@extends('layouts.app')
@section('content')
<div class="container py-4">
<x-edit-profile-modal
:user="$user"
:formAction="route('profile.update')"
formMethod="PUT"
:cancelUrl="route('profile.show')"
:uploadUrl="route('profile.upload-picture')"
/>
</div>
@endsection

File diff suppressed because it is too large Load Diff

View File

@ -27,7 +27,6 @@
.cropme-wrapper { overflow: hidden !important; border-radius: 8px; }
.cropme-slider { display: none !important; }
.takeone-canvas {
height: 400px;
background: #111;
border-radius: 8px;
position: relative;
@ -117,15 +116,15 @@
@push('modals')
<div class="modal fade" id="cropperModal_{{ $id }}" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" style="max-width: 75%; width: 1000px;">
<div class="modal-content modal-content-clean shadow-lg">
<div class="modal-body p-4 text-start">
<div class="modal-body p-4 text-start" style="max-height: 85vh; overflow-y: auto;">
<div class="mb-3 d-flex align-items-center">
<input type="file" id="input_{{ $id }}" class="form-control form-control-sm" accept="image/*">
<button type="button" class="btn-close ms-2" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div id="box_{{ $id }}" class="takeone-canvas"></div>
<div id="box_{{ $id }}" class="takeone-canvas" style="height: 500px;"></div>
<div class="row mt-4">
<div class="col-md-6 mb-3">
@ -172,7 +171,7 @@ $(function() {
if (cropper_{{ $id }}) cropper_{{ $id }}.destroy();
cropper_{{ $id }} = new Cropme(el_{{ $id }}, {
container: { width: '100%', height: 400 },
container: { width: '100%', height: 500 },
viewport: {
width: {{ $width }},
height: {{ $height }},
@ -265,8 +264,19 @@ $(function() {
}).done((res) => {
$('#cropperModal_{{ $id }}').modal('hide');
Toast.success('Photo Updated!', 'Your image has been saved successfully.');
// Reload page after toast shows
setTimeout(() => location.reload(), 1500);
// Update the profile picture in the current page without reload
if (res.url) {
// Update all profile picture images on the page
$('img[src*="profile_{{ str_replace("profile_picture", "", $id) }}"]').attr('src', res.url + '?t=' + new Date().getTime());
// Update any background images
$('[style*="profile_{{ str_replace("profile_picture", "", $id) }}"]').each(function() {
const style = $(this).attr('style');
if (style && style.includes('background-image')) {
$(this).attr('style', style.replace(/url\([^)]+\)/, 'url(' + res.url + '?t=' + new Date().getTime() + ')'));
}
});
}
}).fail((err) => {
Toast.error('Upload Failed', err.responseJSON?.message || 'An error occurred while uploading.');
}).always(() => {

View File

@ -4,6 +4,7 @@ use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use App\Http\Controllers\FamilyController;
use App\Http\Controllers\MemberController;
use App\Http\Controllers\InvoiceController;
use App\Http\Controllers\ClubController;
use App\Http\Controllers\InstructorReviewController;
@ -99,6 +100,14 @@ Route::middleware(['auth', 'verified', 'role:super-admin'])->prefix('admin')->na
// All Members Management
Route::get('/members', [App\Http\Controllers\Admin\PlatformController::class, 'members'])->name('platform.members');
Route::get('/members/{id}', [App\Http\Controllers\Admin\PlatformController::class, 'showMember'])->name('platform.members.show');
Route::get('/members/{id}/edit', [App\Http\Controllers\Admin\PlatformController::class, 'editMember'])->name('platform.members.edit');
Route::put('/members/{id}', [App\Http\Controllers\Admin\PlatformController::class, 'updateMember'])->name('platform.members.update');
Route::delete('/members/{id}', [App\Http\Controllers\Admin\PlatformController::class, 'destroyMember'])->name('platform.members.destroy');
Route::post('/members/{id}/upload-picture', [App\Http\Controllers\Admin\PlatformController::class, 'uploadMemberPicture'])->name('platform.members.upload-picture');
Route::post('/members/{id}/health', [App\Http\Controllers\Admin\PlatformController::class, 'storeMemberHealth'])->name('platform.members.store-health');
Route::put('/members/{id}/health/{recordId}', [App\Http\Controllers\Admin\PlatformController::class, 'updateMemberHealth'])->name('platform.members.update-health');
Route::post('/members/{id}/tournament', [App\Http\Controllers\Admin\PlatformController::class, 'storeMemberTournament'])->name('platform.members.store-tournament');
// Database Backup & Restore
Route::get('/backup', [App\Http\Controllers\Admin\PlatformController::class, 'backup'])->name('platform.backup');
@ -135,24 +144,54 @@ Route::middleware(['auth', 'verified'])->prefix('admin/club/{club}')->name('admi
Route::get('/analytics', [App\Http\Controllers\Admin\ClubAdminController::class, 'analytics'])->name('analytics');
});
// Family routes
// Unified Member routes
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/profile', [FamilyController::class, 'profile'])->name('profile.show');
Route::get('/profile/edit', [FamilyController::class, 'editProfile'])->name('profile.edit');
Route::put('/profile', [FamilyController::class, 'updateProfile'])->name('profile.update');
Route::post('/profile/upload-picture', [FamilyController::class, 'uploadProfilePicture'])->name('profile.upload-picture');
Route::get('/family', [FamilyController::class, 'dashboard'])->name('family.dashboard');
Route::get('/family/create', [FamilyController::class, 'create'])->name('family.create');
Route::post('/family', [FamilyController::class, 'store'])->name('family.store');
Route::get('/family/{id}', [FamilyController::class, 'show'])->name('family.show');
Route::get('/family/{id}/edit', [FamilyController::class, 'edit'])->name('family.edit');
Route::put('/family/{id}', [FamilyController::class, 'update'])->name('family.update');
Route::post('/family/{id}/health', [FamilyController::class, 'storeHealth'])->name('family.store-health');
Route::put('/family/{id}/health/{recordId}', [FamilyController::class, 'updateHealth'])->name('family.update-health');
Route::put('/family/goal/{goalId}', [FamilyController::class, 'updateGoal'])->name('family.update-goal');
Route::post('/family/{id}/tournament', [FamilyController::class, 'storeTournament'])->name('family.store-tournament');
Route::post('/family/{id}/upload-picture', [FamilyController::class, 'uploadFamilyMemberPicture'])->name('family.upload-picture');
Route::delete('/family/{id}', [FamilyController::class, 'destroy'])->name('family.destroy');
// Redirect old /profile route to /member/{id}
Route::get('/profile', function () {
return redirect()->route('member.show', Auth::id());
});
// Redirect old /family route to /members
Route::get('/family', function () {
return redirect()->route('members.index');
});
// Members listing (family dashboard)
Route::get('/members', [MemberController::class, 'index'])->name('members.index');
Route::get('/members/create', [MemberController::class, 'create'])->name('members.create');
Route::post('/members', [MemberController::class, 'store'])->name('members.store');
// Individual member routes
Route::get('/member/{id}', [MemberController::class, 'show'])->name('member.show');
Route::get('/member/{id}/edit', [MemberController::class, 'edit'])->name('member.edit');
Route::put('/member/{id}', [MemberController::class, 'update'])->name('member.update');
Route::delete('/member/{id}', [MemberController::class, 'destroy'])->name('member.destroy');
Route::post('/member/{id}/upload-picture', [MemberController::class, 'uploadPicture'])->name('member.upload-picture');
Route::post('/member/{id}/health', [MemberController::class, 'storeHealth'])->name('member.store-health');
Route::put('/member/{id}/health/{recordId}', [MemberController::class, 'updateHealth'])->name('member.update-health');
Route::post('/member/{id}/tournament', [MemberController::class, 'storeTournament'])->name('member.store-tournament');
Route::put('/member/goal/{goalId}', [MemberController::class, 'updateGoal'])->name('member.update-goal');
// Keep old family routes for backward compatibility (redirect to new routes)
Route::get('/family/create', function () {
return redirect()->route('members.create');
})->name('family.create');
Route::get('/family/{id}', function ($id) {
return redirect()->route('member.show', $id);
})->name('family.show');
Route::get('/family/{id}/edit', function ($id) {
return redirect()->route('member.edit', $id);
})->name('family.edit');
Route::put('/family/{id}', [MemberController::class, 'update'])->name('family.update');
Route::post('/family/{id}/health', [MemberController::class, 'storeHealth'])->name('family.store-health');
Route::put('/family/{id}/health/{recordId}', [MemberController::class, 'updateHealth'])->name('family.update-health');
Route::put('/family/goal/{goalId}', [MemberController::class, 'updateGoal'])->name('family.update-goal');
Route::post('/family/{id}/tournament', [MemberController::class, 'storeTournament'])->name('family.store-tournament');
Route::post('/family/{id}/upload-picture', [MemberController::class, 'uploadPicture'])->name('family.upload-picture');
Route::delete('/family/{id}', [MemberController::class, 'destroy'])->name('family.destroy');
Route::get('/family/dashboard', function () {
return redirect()->route('members.index');
})->name('family.dashboard');
// Bills routes
Route::get('/bills', [InvoiceController::class, 'index'])->name('bills.index');