diff --git a/TODO.md b/TODO.md
index ffc1eea..a46d586 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,35 +1,9 @@
-# Health Section Dynamic Update TODO
-
-## Completed
-- [x] Create HealthRecord model
-- [x] Create migration for health_records table
-- [x] Add healthRecords relationship to User model
-- [x] Update FamilyController show/profile methods to fetch health data
-- [x] Update show.blade.php health tab with dynamic data
- - [x] Replace hardcoded metrics with latest record data
- - [x] Add date dropdowns for comparison (From/To labels)
- - [x] Update comparison table with dynamic changes and colored arrows
- - [x] Update history table with paginated data
-- [x] Run migration
-- [x] Handle no health records case
-- [x] Add health update modal
- - [x] Create modal HTML with form (defaults to current date)
- - [x] Add JavaScript to trigger modal
- - [x] Add route and controller method for storing
- - [x] Handle form submission with validation (at least one metric required)
- - [x] Add flash message display
- - [x] Auto-activate health tab after saving
-- [x] Handle self-profile health updates (no relationship check needed)
-- [x] Add edit functionality for health records
- - [x] Add hover effect with floating pencil icon on history table rows
- - [x] Add JavaScript to populate modal for editing
- - [x] Add route and controller method for updating
- - [x] Handle form submission for updates with validation
- - [x] Update modal title and button text for edit mode
-
-## Testing
-- [x] Test dynamic display with sample data
-- [x] Test modal submission and tab activation
-- [x] Test dynamic comparison dropdowns with live updates, colored arrows, and time difference calculation
-- [x] Test pagination in history table
-- [x] Test edit functionality with hover pencil icon and modal population
+- [x] Add icon to Body Composition Analysis
+- [x] Add icon to Compare
+- [x] Change Health Tracking History to Health Tracking and add icon
+- [x] Add icon to Goals & Progress
+- [x] Add icon to Attendance Records
+- [x] Add icon to Tournament History
+- [x] Add icon to Event Participation
+- [x] Add icon to Affiliations & Badges
+- [x] Change Affiliations tab and header icon from bi-trophy to bi-diagram-3
diff --git a/app/Http/Controllers/FamilyController.php b/app/Http/Controllers/FamilyController.php
index e606afc..69647b2 100644
--- a/app/Http/Controllers/FamilyController.php
+++ b/app/Http/Controllers/FamilyController.php
@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Models\User;
use App\Models\UserRelationship;
use App\Models\HealthRecord;
+use App\Models\Invoice;
use App\Services\FamilyService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@@ -28,6 +29,7 @@ class FamilyController extends Controller
$user = Auth::user();
$dependents = UserRelationship::where('guardian_user_id', $user->id)
->with('dependent')
+ ->whereHas('dependent')
->get()
->sortBy(function($relationship) {
return $relationship->dependent->full_name;
@@ -50,6 +52,9 @@ class FamilyController extends Controller
$healthRecords = $user->healthRecords()->orderBy('recorded_at', 'desc')->paginate(10);
$comparisonRecords = $user->healthRecords()->orderBy('recorded_at', 'desc')->take(2)->get();
+ // Fetch invoices
+ $invoices = Invoice::where('student_user_id', $user->id)->orWhere('payer_user_id', $user->id)->with(['student', 'tenant'])->get();
+
// Pass user directly and a flag to indicate it's the current user's profile
return view('family.show', [
'relationship' => (object)[
@@ -61,6 +66,7 @@ class FamilyController extends Controller
'latestHealthRecord' => $latestHealthRecord,
'healthRecords' => $healthRecords,
'comparisonRecords' => $comparisonRecords,
+ 'invoices' => $invoices,
]);
}
@@ -225,7 +231,10 @@ class FamilyController extends Controller
$healthRecords = $relationship->dependent->healthRecords()->orderBy('recorded_at', 'desc')->paginate(10);
$comparisonRecords = $relationship->dependent->healthRecords()->orderBy('recorded_at', 'desc')->take(2)->get();
- return view('family.show', compact('relationship', 'latestHealthRecord', 'healthRecords', 'comparisonRecords'));
+ // Fetch invoices for the dependent
+ $invoices = Invoice::where('student_user_id', $relationship->dependent->id)->orWhere('payer_user_id', $relationship->dependent->id)->with(['student', 'tenant'])->get();
+
+ return view('family.show', compact('relationship', 'latestHealthRecord', 'healthRecords', 'comparisonRecords', 'invoices'));
}
/**
diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php
index 4bf9dab..778e39b 100644
--- a/app/Http/Controllers/InvoiceController.php
+++ b/app/Http/Controllers/InvoiceController.php
@@ -53,9 +53,9 @@ class InvoiceController extends Controller
* Display the receipt for the specified invoice.
*
* @param int $id
- * @return \Illuminate\View\View
+ * @return \Illuminate\View\View|\Illuminate\Http\Response
*/
- public function receipt($id)
+ public function receipt(Request $request, $id)
{
$user = Auth::user();
$invoice = Invoice::where('id', $id)
@@ -63,6 +63,13 @@ class InvoiceController extends Controller
->with(['student', 'tenant'])
->firstOrFail();
+ if ($request->has('download')) {
+ $html = view('invoices.receipt', compact('invoice'))->render();
+ return response($html)
+ ->header('Content-Type', 'text/html')
+ ->header('Content-Disposition', 'attachment; filename="receipt_' . $invoice->id . '.html"');
+ }
+
return view('invoices.receipt', compact('invoice'));
}
diff --git a/app/Models/User.php b/app/Models/User.php
index da9186d..06232b0 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -3,7 +3,6 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
-use Illuminate\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
@@ -16,7 +15,7 @@ use Carbon\Carbon;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
- use HasFactory, Notifiable, MustVerifyEmail, SoftDeletes;
+ use HasFactory, Notifiable, SoftDeletes;
/**
* The attributes that are mass assignable.
diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php
index b28fd5a..df2f429 100644
--- a/database/seeders/DatabaseSeeder.php
+++ b/database/seeders/DatabaseSeeder.php
@@ -27,6 +27,7 @@ class DatabaseSeeder extends Seeder
'full_name' => 'Test User',
'mobile' => ['code' => '+1', 'number' => '1234567890'],
'password' => bcrypt('password'),
+ 'email_verified_at' => now(),
]);
// Create a tenant (club)
@@ -34,6 +35,8 @@ class DatabaseSeeder extends Seeder
'owner_user_id' => $user->id,
'club_name' => 'Test Club',
'slug' => 'test-club',
+ 'gps_lat' => 25.276987, // Dubai latitude
+ 'gps_long' => 55.296249, // Dubai longitude
]);
// Create membership
diff --git a/package-lock.json b/package-lock.json
index 506d85b..1be44d8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5,6 +5,7 @@
"packages": {
"": {
"dependencies": {
+ "chart.js": "^4.5.1",
"jquery-cropbox": "github:acornejo/jquery-cropbox"
},
"devDependencies": {
@@ -479,6 +480,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+ "license": "MIT"
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.55.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.2.tgz",
@@ -1158,6 +1165,18 @@
"node": ">=8"
}
},
+ "node_modules/chart.js": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
+ "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -1924,6 +1943,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -2150,6 +2170,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
+ "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -2520,6 +2541,11 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="
+ },
"@rollup/rollup-android-arm-eabi": {
"version": "4.55.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.2.tgz",
@@ -2911,6 +2937,14 @@
}
}
},
+ "chart.js": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
+ "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
+ "requires": {
+ "@kurkle/color": "^0.3.0"
+ }
+ },
"cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -3207,7 +3241,7 @@
},
"jquery-cropbox": {
"version": "git+ssh://git@github.com/acornejo/jquery-cropbox.git#5d4ec5290849507ee27c11e8ae337090182ade31",
- "from": "jquery-cropbox@https://github.com/acornejo/jquery-cropbox.git"
+ "from": "jquery-cropbox@github:acornejo/jquery-cropbox"
},
"laravel-vite-plugin": {
"version": "2.1.0",
@@ -3362,7 +3396,8 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"postcss": {
"version": "8.5.6",
@@ -3517,6 +3552,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
+ "peer": true,
"requires": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
diff --git a/package.json b/package.json
index d6c4beb..c242851 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"vite": "^7.0.7"
},
"dependencies": {
+ "chart.js": "^4.5.1",
"jquery-cropbox": "github:acornejo/jquery-cropbox"
}
}
diff --git a/resources/js/app.js b/resources/js/app.js
index cbc6d56..8857701 100644
--- a/resources/js/app.js
+++ b/resources/js/app.js
@@ -2,7 +2,9 @@ import './bootstrap';
import $ from 'jquery';
import 'jquery-cropbox';
import imageCompression from 'browser-image-compression';
+import Chart from 'chart.js/auto';
// Make jQuery globally available for jquery-cropbox
window.$ = window.jQuery = $;
window.imageCompression = imageCompression;
+window.Chart = Chart;
diff --git a/resources/views/clubs/explore.blade.php b/resources/views/clubs/explore.blade.php
index 114f7ba..901cf17 100644
--- a/resources/views/clubs/explore.blade.php
+++ b/resources/views/clubs/explore.blade.php
@@ -97,6 +97,8 @@
+
+
@@ -317,15 +319,20 @@ document.addEventListener('DOMContentLoaded', function() {
// Near Me button
document.getElementById('nearMeBtn').addEventListener('click', function() {
const mapModal = new bootstrap.Modal(document.getElementById('mapModal'));
- mapModal.show();
+ const modalElement = document.getElementById('mapModal');
- // Initialize map when modal is shown
- setTimeout(() => {
+ modalElement.addEventListener('shown.bs.modal', function() {
if (userLocation) {
initMap(userLocation.latitude, userLocation.longitude);
updateModalLocation(userLocation.latitude, userLocation.longitude);
+ } else {
+ // No location available, use default and let user drag
+ initMap(25.276987, 55.296249); // Default to Dubai or any location
+ updateModalLocation(25.276987, 55.296249);
}
- }, 300);
+ }, { once: true });
+
+ mapModal.show();
});
// Apply Location button
@@ -354,10 +361,11 @@ function startWatchingLocation() {
updateLocationDisplay(userLocation.latitude, userLocation.longitude);
- // If current category is not 'all', fetch nearby clubs
- if (currentCategory !== 'all') {
- fetchNearbyClubs(userLocation.latitude, userLocation.longitude);
- }
+ // Fetch nearby clubs
+ fetchNearbyClubs(userLocation.latitude, userLocation.longitude);
+
+ // Initialize page map
+ initPageMap(userLocation.latitude, userLocation.longitude);
// Stop watching after first successful location
if (watchId) {
@@ -398,6 +406,43 @@ function updateLocationDisplay(lat, lng) {
`${lat.toFixed(4)}, ${lng.toFixed(4)}`;
}
+// Initialize page map
+function initPageMap(lat, lng) {
+ if (pageMap) {
+ pageMap.remove();
+ }
+
+ pageMap = L.map('pageMap').setView([lat, lng], 13);
+
+ L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ attribution: '© OpenStreetMap contributors',
+ maxZoom: 19
+ }).addTo(pageMap);
+
+ // Add user marker
+ userMarker = L.marker([lat, lng], {
+ icon: L.divIcon({
+ className: 'user-location-marker',
+ html: '',
+ iconSize: [36, 36],
+ iconAnchor: [18, 36]
+ })
+ }).addTo(pageMap);
+
+ // Add club markers
+ if (allClubs.length > 0) {
+ allClubs.forEach(club => {
+ if (club.gps_lat && club.gps_long) {
+ L.marker([club.gps_lat, club.gps_long]).addTo(pageMap)
+ .bindPopup(`${club.club_name}
${club.owner_name || 'N/A'}`);
+ }
+ });
+ }
+
+ setTimeout(() => pageMap.invalidateSize(), 100);
+ document.getElementById('mapSection').style.display = 'block';
+}
+
// Initialize map in modal
function initMap(lat, lng) {
if (map) {
@@ -406,7 +451,7 @@ function initMap(lat, lng) {
map = L.map('map', { attributionControl: false }).setView([lat, lng], 13);
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
maxZoom: 19
}).addTo(map);
@@ -423,7 +468,7 @@ function initMap(lat, lng) {
}).addTo(map);
// Drag event
- userMarker.on('dragend', function(event) {
+ userMarker.on('drag', function(event) {
const position = event.target.getLatLng();
userLocation = {
latitude: position.lat,
@@ -520,6 +565,24 @@ function displayClubs(clubs) {
noResultsContainer.style.display = 'none';
+ // Update map markers if page map exists
+ if (pageMap) {
+ // Clear existing club markers (assuming user marker is the first)
+ pageMap.eachLayer(function(layer) {
+ if (layer instanceof L.Marker && layer !== userMarker) {
+ pageMap.removeLayer(layer);
+ }
+ });
+
+ // Add new club markers
+ clubs.forEach(club => {
+ if (club.gps_lat && club.gps_long) {
+ L.marker([club.gps_lat, club.gps_long]).addTo(pageMap)
+ .bindPopup(`${club.club_name}
${club.owner_name || 'N/A'}`);
+ }
+ });
+ }
+
clubs.forEach(club => {
const card = document.createElement('div');
card.className = 'col-md-6 col-lg-4';
diff --git a/resources/views/family/dashboard.blade.php b/resources/views/family/dashboard.blade.php
index 591a46d..6e4d597 100644
--- a/resources/views/family/dashboard.blade.php
+++ b/resources/views/family/dashboard.blade.php
@@ -8,122 +8,7 @@
Revenue analytics over time
+Self investment analytics over time
Attendance tracking coming soon...
Radar chart visualization coming soon...
- Chart will compare current vs previous body composition metrics +Goal tracking coming soon...
Achievement system coming soon...
+Affiliation system coming soon...
Tournament records coming soon...
Event history coming soon...