From b16351c4166c87985a2bb5a3b46ecc25e65c4034 Mon Sep 17 00:00:00 2001 From: GhassanYusuf Date: Mon, 26 Jan 2026 00:50:43 +0300 Subject: [PATCH] created admin panel with some of its content --- ADMIN_ACCESS_GUIDE.md | 143 ++++++ ADMIN_LAYOUT_UPDATE_SUMMARY.md | 228 ++++++++++ ADMIN_PANEL_PROGRESS.md | 295 ++++++++++++ EXPLORE_LOCATION_SORTING_UPDATE.md | 117 +++++ TESTING_ADMIN_PANEL.md | 425 ++++++++++++++++++ TODO_ADMIN_PANEL.md | 371 +++++++++++++++ app/Console/Commands/MakeSuperAdmin.php | 53 +++ .../Controllers/Admin/PlatformController.php | 338 ++++++++++++++ app/Http/Controllers/ClubController.php | 73 ++- app/Http/Middleware/CheckPermission.php | 35 ++ app/Http/Middleware/CheckRole.php | 35 ++ app/Models/ClubActivity.php | 72 +++ app/Models/ClubBankAccount.php | 104 +++++ app/Models/ClubFacility.php | 62 +++ app/Models/ClubGalleryImage.php | 57 +++ app/Models/ClubInstructor.php | 71 +++ app/Models/ClubMemberSubscription.php | 112 +++++ app/Models/ClubMessage.php | 95 ++++ app/Models/ClubPackage.php | 90 ++++ app/Models/ClubReview.php | 74 +++ app/Models/ClubSocialLink.php | 49 ++ app/Models/ClubTransaction.php | 95 ++++ app/Models/Permission.php | 32 ++ app/Models/Role.php | 50 +++ app/Models/Tenant.php | 144 +++++- app/Models/User.php | 148 ++++++ bootstrap/app.php | 5 +- ...00_create_roles_and_permissions_tables.php | 64 +++ ...2026_01_25_100001_expand_tenants_table.php | 84 ++++ ...25_100002_create_club_facilities_table.php | 36 ++ ...5_100003_create_club_instructors_table.php | 38 ++ ...25_100004_create_club_activities_table.php | 43 ++ ...1_25_100005_create_club_packages_table.php | 41 ++ ...6_create_club_package_activities_table.php | 34 ++ ...create_club_member_subscriptions_table.php | 43 ++ ..._100008_create_club_transactions_table.php | 43 ++ ...00009_create_club_gallery_images_table.php | 35 ++ ..._100010_create_club_social_links_table.php | 34 ++ ...100011_create_club_bank_accounts_table.php | 36 ++ ...1_25_100012_create_club_messages_table.php | 39 ++ ...01_25_100013_create_club_reviews_table.php | 37 ++ database/seeders/RolePermissionSeeder.php | 123 +++++ .../views/admin/platform/backup.blade.php | 195 ++++++++ .../views/admin/platform/clubs.blade.php | 198 ++++++++ .../admin/platform/create-club.blade.php | 187 ++++++++ .../views/admin/platform/edit-club.blade.php | 187 ++++++++ .../views/admin/platform/index.blade.php | 185 ++++++++ .../views/admin/platform/members.blade.php | 281 ++++++++++++ resources/views/clubs/explore.blade.php | 296 +++++------- resources/views/layouts/admin.blade.php | 165 +++++++ resources/views/layouts/app.blade.php | 74 ++- routes/web.php | 22 + 52 files changed, 5690 insertions(+), 203 deletions(-) create mode 100644 ADMIN_ACCESS_GUIDE.md create mode 100644 ADMIN_LAYOUT_UPDATE_SUMMARY.md create mode 100644 ADMIN_PANEL_PROGRESS.md create mode 100644 EXPLORE_LOCATION_SORTING_UPDATE.md create mode 100644 TESTING_ADMIN_PANEL.md create mode 100644 TODO_ADMIN_PANEL.md create mode 100644 app/Console/Commands/MakeSuperAdmin.php create mode 100644 app/Http/Controllers/Admin/PlatformController.php create mode 100644 app/Http/Middleware/CheckPermission.php create mode 100644 app/Http/Middleware/CheckRole.php create mode 100644 app/Models/ClubActivity.php create mode 100644 app/Models/ClubBankAccount.php create mode 100644 app/Models/ClubFacility.php create mode 100644 app/Models/ClubGalleryImage.php create mode 100644 app/Models/ClubInstructor.php create mode 100644 app/Models/ClubMemberSubscription.php create mode 100644 app/Models/ClubMessage.php create mode 100644 app/Models/ClubPackage.php create mode 100644 app/Models/ClubReview.php create mode 100644 app/Models/ClubSocialLink.php create mode 100644 app/Models/ClubTransaction.php create mode 100644 app/Models/Permission.php create mode 100644 app/Models/Role.php create mode 100644 database/migrations/2026_01_25_100000_create_roles_and_permissions_tables.php create mode 100644 database/migrations/2026_01_25_100001_expand_tenants_table.php create mode 100644 database/migrations/2026_01_25_100002_create_club_facilities_table.php create mode 100644 database/migrations/2026_01_25_100003_create_club_instructors_table.php create mode 100644 database/migrations/2026_01_25_100004_create_club_activities_table.php create mode 100644 database/migrations/2026_01_25_100005_create_club_packages_table.php create mode 100644 database/migrations/2026_01_25_100006_create_club_package_activities_table.php create mode 100644 database/migrations/2026_01_25_100007_create_club_member_subscriptions_table.php create mode 100644 database/migrations/2026_01_25_100008_create_club_transactions_table.php create mode 100644 database/migrations/2026_01_25_100009_create_club_gallery_images_table.php create mode 100644 database/migrations/2026_01_25_100010_create_club_social_links_table.php create mode 100644 database/migrations/2026_01_25_100011_create_club_bank_accounts_table.php create mode 100644 database/migrations/2026_01_25_100012_create_club_messages_table.php create mode 100644 database/migrations/2026_01_25_100013_create_club_reviews_table.php create mode 100644 database/seeders/RolePermissionSeeder.php create mode 100644 resources/views/admin/platform/backup.blade.php create mode 100644 resources/views/admin/platform/clubs.blade.php create mode 100644 resources/views/admin/platform/create-club.blade.php create mode 100644 resources/views/admin/platform/edit-club.blade.php create mode 100644 resources/views/admin/platform/index.blade.php create mode 100644 resources/views/admin/platform/members.blade.php create mode 100644 resources/views/layouts/admin.blade.php diff --git a/ADMIN_ACCESS_GUIDE.md b/ADMIN_ACCESS_GUIDE.md new file mode 100644 index 0000000..991746f --- /dev/null +++ b/ADMIN_ACCESS_GUIDE.md @@ -0,0 +1,143 @@ +# Admin Panel Access Guide + +## How to Access the Admin Dashboard + +### Step 1: Make Your User a Super Admin + +Run this command in your terminal (replace with your email): + +```bash +php artisan admin:make-super your-email@example.com +``` + +**Example:** +```bash +php artisan admin:make-super admin@takeone.com +``` + +You should see: +``` +✅ Successfully made 'Your Name' (your-email@example.com) a super admin! +They can now access the admin panel at: http://localhost:8000/admin +``` + +--- + +### Step 2: Login to Your Account + +1. Go to: `http://localhost:8000/login` +2. Login with your credentials +3. After successful login, you'll be redirected to the explore page + +--- + +### Step 3: Access the Admin Panel + +Once logged in as a super admin, navigate to: + +``` +http://localhost:8000/admin +``` + +Or click on "Admin Panel" link in the user dropdown menu (if you add it to the main layout). + +--- + +## Admin Panel Features + +### Available Routes: + +1. **Dashboard** - `/admin` + - Platform statistics + - Quick action cards + +2. **All Clubs** - `/admin/clubs` + - View all clubs in grid layout + - Search clubs + - Create new club + - Edit existing clubs + - Delete clubs + +3. **All Members** - `/admin/members` + - View all platform members + - Search members + - View member details + +4. **Database Backup** - `/admin/backup` + - Download full database backup (JSON) + - Restore from backup + - Export authentication users + +--- + +## Troubleshooting + +### "403 Unauthorized" Error + +If you see a 403 error when accessing `/admin`, it means: +- You're not logged in, OR +- Your user doesn't have the super-admin role + +**Solution:** +1. Make sure you're logged in +2. Run the command: `php artisan admin:make-super your-email@example.com` +3. Logout and login again +4. Try accessing `/admin` again + +### "Role not found" Error + +If the command fails with "Role not found": + +**Solution:** +Run the seeder to create roles: +```bash +php artisan db:seed --class=RolePermissionSeeder +``` + +Then try the make-super command again. + +--- + +## Quick Test Checklist + +After getting access, test these features: + +- [ ] Dashboard loads with statistics +- [ ] Navigate to All Clubs page +- [ ] Search for clubs (if any exist) +- [ ] Click "Add New Club" button +- [ ] Navigate to All Members page +- [ ] Search for members +- [ ] Navigate to Database Backup page +- [ ] Check sidebar navigation works +- [ ] Test responsive design (resize browser) + +--- + +## Adding Admin Link to Main Navigation + +To make it easier to access the admin panel, you can add a link in the main layout. + +Edit `resources/views/layouts/app.blade.php` and add this in the user dropdown: + +```blade +@if(Auth::user()->isSuperAdmin()) + + Admin Panel + + +@endif +``` + +--- + +## Security Notes + +- Only users with the `super-admin` role can access `/admin` routes +- All admin routes are protected with `role:super-admin` middleware +- Destructive actions (delete, restore) have confirmation dialogs +- Bank account information is encrypted in the database + +--- + +**Need Help?** Check the ADMIN_PANEL_PROGRESS.md file for implementation details. diff --git a/ADMIN_LAYOUT_UPDATE_SUMMARY.md b/ADMIN_LAYOUT_UPDATE_SUMMARY.md new file mode 100644 index 0000000..193b084 --- /dev/null +++ b/ADMIN_LAYOUT_UPDATE_SUMMARY.md @@ -0,0 +1,228 @@ +# Admin Panel Layout Update - Complete ✅ + +## Summary of Changes + +Successfully updated the admin panel to use the same boxed layout as the explore page with consistent top navigation bar. + +--- + +## Files Modified + +### 1. Main Layout +- **`resources/views/layouts/app.blade.php`** + - Added "Admin Panel" link in user dropdown + - Only visible to users with super-admin role + - Uses `Auth::user()->isSuperAdmin()` method + +### 2. Admin Pages (All Updated) +All 6 admin pages now use `@extends('layouts.app')` with boxed container: + +1. **`resources/views/admin/platform/index.blade.php`** - Dashboard + - ✅ Boxed layout with `
` + - ✅ Clickable stat cards (Total Clubs, Total Members, Active Clubs) + - ✅ Circular icon backgrounds for quick action cards + - ✅ Hover effects on all cards + - ✅ "Back to Explore" link + +2. **`resources/views/admin/platform/clubs.blade.php`** - All Clubs + - ✅ Boxed layout + - ✅ Search bar with red "Add New Club" button + - ✅ Club cards with cover images and circular logos + - ✅ "Back to Dashboard" and "Back to Explore" links + +3. **`resources/views/admin/platform/create-club.blade.php`** - Create Club + - ✅ Boxed layout + - ✅ Centered page header + - ✅ Form with shadow-sm card + - ✅ Primary blue submit button + +4. **`resources/views/admin/platform/edit-club.blade.php`** - Edit Club + - ✅ Boxed layout + - ✅ Centered page header + - ✅ Pre-filled form fields + - ✅ Primary blue update button + +5. **`resources/views/admin/platform/members.blade.php`** - All Members + - ✅ Boxed layout + - ✅ Member cards with avatars + - ✅ Search functionality + - ✅ Navigation links + +6. **`resources/views/admin/platform/backup.blade.php`** - Database Backup + - ✅ Boxed layout + - ✅ Three operation cards + - ✅ Warning messages + - ✅ Navigation links + +--- + +## Key Features Implemented + +### 1. Consistent Layout +- All admin pages use the same top navigation bar as explore page +- Boxed container (`
`) +- Consistent card styling with `border-0 shadow-sm` +- Uniform spacing and padding + +### 2. Clickable Statistics Cards +- **Total Clubs** → Links to `/admin/clubs` +- **Total Members** → Links to `/admin/members` +- **Active Clubs** → Links to `/admin/clubs?status=active` +- **Total Revenue** → Display only (no link) + +### 3. Circular Icon Backgrounds +- Quick action cards have circular icon containers +- Size: 80px × 80px +- Centered icons with proper alignment +- Color-coded backgrounds (primary, success, warning) + +### 4. Hover Effects +- All cards have `hover-lift` class +- Smooth transform and shadow transitions +- Cursor changes to pointer on clickable elements + +### 5. Navigation +- "Admin Panel" link in user dropdown (super admin only) +- "Back to Dashboard" links on sub-pages +- "Back to Explore" links on all pages +- Breadcrumb-style navigation + +--- + +## Access Control + +### How to Access Admin Panel + +1. **Make a user super admin:** + ```bash + php artisan admin:make-super your-email@example.com + ``` + +2. **Login and access:** + - Login at: `http://localhost:8000/login` + - Click user dropdown in top-right + - Click "Admin Panel" (only visible to super admins) + - Or navigate directly to: `http://localhost:8000/admin` + +### User Dropdown Visibility +- **Super Admin:** Sees "Admin Panel" link +- **Regular User:** Does NOT see "Admin Panel" link +- **Non-authenticated:** Redirected to login + +--- + +## Visual Improvements + +### Before +- Separate admin layout with sidebar +- Different styling from main app +- Inconsistent navigation + +### After +- ✅ Same top bar as explore page +- ✅ Boxed container layout +- ✅ Consistent Bootstrap 5 styling +- ✅ Circular icon backgrounds +- ✅ Clickable stat cards +- ✅ Hover effects throughout +- ✅ Seamless navigation between admin and user views + +--- + +## Button Styling + +### Primary Actions +- **Color:** Primary blue (`btn-primary`) +- **Usage:** Submit forms, main actions +- **Examples:** "Create Club", "Update Club", "Go to Clubs" + +### Danger Actions +- **Color:** Red (`btn-danger`) +- **Usage:** Add new items, destructive actions +- **Examples:** "Add New Club", "Delete", "Restore Database" + +### Secondary Actions +- **Color:** Gray (`btn-secondary`, `btn-outline-primary`) +- **Usage:** Cancel, back navigation +- **Examples:** "Cancel", "Back to Dashboard" + +--- + +## Responsive Design + +All pages are fully responsive: +- **Mobile (< 768px):** Cards stack vertically +- **Tablet (768px - 1024px):** 2-column grid +- **Desktop (> 1024px):** 3-4 column grid +- Container stays boxed on all screen sizes + +--- + +## Testing Checklist + +### ✅ Completed +- [x] Layout structure updated +- [x] All pages use app layout +- [x] Stat cards made clickable +- [x] Icon circles implemented +- [x] Hover effects added +- [x] Navigation links added +- [x] Admin Panel link in dropdown + +### 🔍 Ready for Testing +- [ ] Login as super admin +- [ ] Verify "Admin Panel" link appears +- [ ] Click stat cards (Total Clubs, Total Members, Active Clubs) +- [ ] Test all navigation links +- [ ] Verify responsive design +- [ ] Test on different browsers +- [ ] Verify access control (non-admin users) + +--- + +## Next Steps + +1. **Test the admin panel:** + - Make yourself super admin: `php artisan admin:make-super your-email@example.com` + - Login and navigate to admin panel + - Click through all pages + - Test clickable cards + - Verify responsive design + +2. **Optional Enhancements:** + - Add loading states + - Implement real-time statistics + - Add more filtering options + - Enhance search functionality + - Add export features + +3. **Continue with Phase 5:** + - Club-level admin dashboard + - Individual club management + - Member management per club + - Financial tracking per club + +--- + +## Documentation Files + +- **ADMIN_ACCESS_GUIDE.md** - How to access admin panel +- **ADMIN_PANEL_PROGRESS.md** - Overall implementation progress +- **TESTING_ADMIN_PANEL.md** - Comprehensive testing guide +- **ADMIN_LAYOUT_UPDATE_SUMMARY.md** - This file + +--- + +## Support + +If you encounter any issues: +1. Check browser console for errors +2. Verify you're logged in as super admin +3. Clear browser cache +4. Run `php artisan config:clear` +5. Run `php artisan view:clear` + +--- + +**Status:** ✅ Complete and Ready for Testing +**Last Updated:** January 2026 diff --git a/ADMIN_PANEL_PROGRESS.md b/ADMIN_PANEL_PROGRESS.md new file mode 100644 index 0000000..7c2167e --- /dev/null +++ b/ADMIN_PANEL_PROGRESS.md @@ -0,0 +1,295 @@ +# TAKEONE Admin Panel - Implementation Progress Report + +**Date:** January 25, 2026 +**Status:** Phase 1-4 COMPLETED ✅ + +--- + +## ✅ COMPLETED PHASES + +### Phase 1: Database Schema Expansion - COMPLETED ✅ + +**All 14 migrations created and executed successfully:** + +1. ✅ `2026_01_25_100000_create_roles_and_permissions_tables.php` + - roles, permissions, role_permission, user_roles tables + - Support for tenant-specific roles + +2. ✅ `2026_01_25_100001_expand_tenants_table.php` + - Added: slogan, description, enrollment_fee, VAT fields + - Added: email, phone (JSON), currency, timezone, country + - Added: favicon, cover_image, owner details + - Added: settings (JSON) for code prefixes + - Added: soft deletes + +3. ✅ `2026_01_25_100002_create_club_facilities_table.php` + - Facilities with GPS coordinates and availability status + +4. ✅ `2026_01_25_100003_create_club_instructors_table.php` + - Instructors with skills (JSON), rating, experience + +5. ✅ `2026_01_25_100004_create_club_activities_table.php` + - Activities with schedule (JSON), duration, frequency + +6. ✅ `2026_01_25_100005_create_club_packages_table.php` + - Packages with age ranges, pricing, type (single/multi) + +7. ✅ `2026_01_25_100006_create_club_package_activities_table.php` + - Pivot table linking packages, activities, and instructors + +8. ✅ `2026_01_25_100007_create_club_member_subscriptions_table.php` + - Subscriptions with payment tracking and status + +9. ✅ `2026_01_25_100008_create_club_transactions_table.php` + - Financial transactions (income/expense/refund) + +10. ✅ `2026_01_25_100009_create_club_gallery_images_table.php` + - Gallery management with display order + +11. ✅ `2026_01_25_100010_create_club_social_links_table.php` + - Social media links with icons + +12. ✅ `2026_01_25_100011_create_club_bank_accounts_table.php` + - Encrypted bank account details (account_number, IBAN, SWIFT) + +13. ✅ `2026_01_25_100012_create_club_messages_table.php` + - Internal messaging with read tracking + +14. ✅ `2026_01_25_100013_create_club_reviews_table.php` + - Club reviews with approval system + +--- + +### Phase 2: Models & Relationships - COMPLETED ✅ + +**All 13 models created with full relationships:** + +1. ✅ `Role.php` - With permissions relationship and hasPermission() method +2. ✅ `Permission.php` - Basic permission model +3. ✅ `ClubFacility.php` - With GPS decimal casting +4. ✅ `ClubInstructor.php` - With skills array casting +5. ✅ `ClubActivity.php` - With schedule JSON casting +6. ✅ `ClubPackage.php` - With activities many-to-many relationship +7. ✅ `ClubMemberSubscription.php` - With expiry checking methods +8. ✅ `ClubTransaction.php` - With type scopes (income/expense/refund) +9. ✅ `ClubGalleryImage.php` - With uploader relationship +10. ✅ `ClubSocialLink.php` - With display order +11. ✅ `ClubBankAccount.php` - With encrypted accessors for sensitive data +12. ✅ `ClubMessage.php` - With read/unread scopes and markAsRead() +13. ✅ `ClubReview.php` - With approved/pending scopes + +**Updated existing models:** + +- ✅ `Tenant.php` - Added 12 new relationships, soft deletes, computed attributes (averageRating, activeMembersCount, url) +- ✅ `User.php` - Added role methods (hasRole, hasPermission, isSuperAdmin, isClubAdmin, isInstructor, assignRole, removeRole) + +--- + +### Phase 3: Role-Based Access Control (RBAC) - COMPLETED ✅ + +**Middleware:** +- ✅ `CheckRole.php` - Role-based access control middleware +- ✅ `CheckPermission.php` - Permission-based access control middleware +- ✅ Registered in `bootstrap/app.php` with aliases: 'role', 'permission' + +**Seeder:** +- ✅ `RolePermissionSeeder.php` - Created and executed + - 4 Roles: Super Admin, Club Admin, Instructor, Member + - 20 Permissions covering all admin operations + - Proper role-permission assignments + +**Helper Methods in User Model:** +- ✅ `hasRole($roleSlug, $tenantId)` - Check specific role +- ✅ `hasAnyRole($roleSlugs, $tenantId)` - Check multiple roles +- ✅ `hasPermission($permissionSlug, $tenantId)` - Check permission +- ✅ `isSuperAdmin()` - Quick super admin check +- ✅ `isClubAdmin($tenantId)` - Quick club admin check +- ✅ `isInstructor($tenantId)` - Quick instructor check +- ✅ `assignRole($roleSlug, $tenantId)` - Assign role to user +- ✅ `removeRole($roleSlug, $tenantId)` - Remove role from user + +--- + +### Phase 4: Platform-Level Admin (Super Admin) - COMPLETED ✅ + +**Controller:** +- ✅ `Admin/PlatformController.php` - Fully implemented with 13 methods: + - `index()` - Dashboard with stats + - `clubs()` - All clubs listing with search + - `createClub()` - Show create form + - `storeClub()` - Store new club + - `editClub()` - Show edit form + - `updateClub()` - Update club + - `destroyClub()` - Delete club + - `members()` - All members listing with search + - `backup()` - Backup page + - `downloadBackup()` - Download JSON backup + - `restoreBackup()` - Restore from JSON + - `exportAuthUsers()` - Export users with passwords + +**Routes (all protected with role:super-admin middleware):** +- ✅ `GET /admin` - Platform dashboard +- ✅ `GET /admin/clubs` - All clubs management +- ✅ `GET /admin/clubs/create` - Create club form +- ✅ `POST /admin/clubs` - Store new club +- ✅ `GET /admin/clubs/{club}/edit` - Edit club form +- ✅ `PUT /admin/clubs/{club}` - Update club +- ✅ `DELETE /admin/clubs/{club}` - Delete club +- ✅ `GET /admin/members` - All members management +- ✅ `GET /admin/backup` - Backup & restore page +- ✅ `GET /admin/backup/download` - Download backup +- ✅ `POST /admin/backup/restore` - Restore backup +- ✅ `GET /admin/backup/export-users` - Export auth users + +**Views:** +- ✅ `layouts/admin.blade.php` - Admin panel layout with: + - Fixed sidebar navigation + - Top navbar with user dropdown + - Alert messages (success/error) + - Responsive design + - Custom admin styling + +- ✅ `admin/platform/index.blade.php` - Dashboard with: + - 4 stat cards (Total Clubs, Total Members, Active Clubs, Total Revenue) + - 3 quick action cards (Manage Clubs, Manage Members, Database Backup) + - Recent activity placeholder + +- ✅ `admin/platform/clubs.blade.php` - All clubs management with: + - Search functionality + - Grid layout with club cards + - Cover images and logos + - Stats per club (members, packages, trainers) + - Owner information + - Edit and delete actions + - Pagination + - Empty state + +- ✅ `admin/platform/members.blade.php` - All members management with: + - Search functionality + - Grid layout with member cards + - Avatar display + - Adult/Child badges + - Club count badges + - Contact information + - Gender, age, nationality display + - Horoscope and birthday countdown + - Member since date + - View and edit actions + - Pagination + - Empty state + +- ✅ `admin/platform/backup.blade.php` - Database backup with: + - 3-column operation layout + - Download full backup (JSON) + - Restore from backup (with warnings) + - Export auth users + - Best practices section + - Restore warnings + - Confirmation modal + - Safety checks + +--- + +## 📊 OVERALL PROGRESS + +**Completed:** Phases 1-4 (40% of total project) +**Status:** Platform-level admin fully functional + +### What's Working: +✅ Complete database schema for admin panel +✅ All Eloquent models with relationships +✅ Role-based access control system +✅ Platform admin dashboard +✅ All clubs management (CRUD with search) +✅ All members management (view with search) +✅ Database backup and restore functionality +✅ Responsive admin UI with Bootstrap 5 +✅ Middleware protection on all admin routes + +--- + +## 🔜 REMAINING PHASES + +### Phase 5: Club-Level Admin Dashboard (NEXT PRIORITY) +- Club admin sidebar layout +- Dashboard with club-specific stats +- 11 management modules (details, gallery, facilities, etc.) + +### Phase 6: Core Features Implementation +- Multi-currency support +- Multi-timezone support +- File upload & management +- Financial system with charts +- Analytics dashboard +- Messaging system + +### Phase 7: Additional Features +- Club details management (6 tabs) +- Gallery, facilities, instructors management +- Activities, packages, members management + +### Phase 8: Components & Reusables +- Blade components for dropdowns +- Reusable UI components + +### Phase 9: Testing & Quality Assurance +- Feature tests +- Seeders for demo data +- Code quality checks + +### Phase 10: Documentation & Deployment +- Documentation +- Deployment preparation + +--- + +## 📝 TECHNICAL NOTES + +**Architecture:** +- Multi-tenancy with tenant_id foreign keys +- Soft deletes on critical tables +- Encrypted sensitive data (bank accounts) +- JSON columns for flexible data (phone, settings, skills, schedule) +- Proper indexing on foreign keys and search fields + +**Security:** +- Role-based middleware on all admin routes +- CSRF protection on all forms +- Encrypted bank account information +- Confirmation dialogs on destructive actions +- Input validation on all forms + +**Performance:** +- Eager loading relationships (with, withCount) +- Pagination on large datasets +- Indexed foreign keys +- Efficient queries with scopes + +**UI/UX:** +- Consistent Bootstrap 5 styling +- Responsive design +- Empty states for better UX +- Loading states and feedback +- Search and filter functionality +- Card-based layouts +- Icon usage throughout + +--- + +## 🎯 NEXT STEPS + +1. **Create club admin layout** with sidebar navigation +2. **Build club dashboard** with stats and charts +3. **Implement club details management** (6 tabs) +4. **Add gallery management** CRUD +5. **Build facilities management** with GPS +6. **Create instructors management** with skills +7. **Implement activities management** with scheduling +8. **Build packages management** with pricing +9. **Add members management** for club +10. **Create financial management** with transactions + +--- + +**Last Updated:** January 25, 2026 +**Next Review:** After Phase 5 completion diff --git a/EXPLORE_LOCATION_SORTING_UPDATE.md b/EXPLORE_LOCATION_SORTING_UPDATE.md new file mode 100644 index 0000000..5974aa1 --- /dev/null +++ b/EXPLORE_LOCATION_SORTING_UPDATE.md @@ -0,0 +1,117 @@ +# Explore Page - Location-Based Sorting Implementation + +## Summary +Implemented location-based sorting for clubs in the `/explore` page. When the "All" or "Clubs" tabs are selected, clubs are now sorted by distance from nearest to farthest based on the user's set location. + +## Changes Made + +### 1. Backend Changes - `app/Http/Controllers/ClubController.php` + +#### Modified `all()` Method +- **Added Parameters**: Now accepts optional `latitude` and `longitude` query parameters +- **Distance Calculation**: When location is provided, calculates distance for each club using the Haversine formula +- **Sorting Logic**: Sorts clubs by distance (nearest first) + - Clubs with GPS coordinates and calculated distance appear first (sorted by distance) + - Clubs without GPS coordinates appear at the end +- **Response**: Returns clubs with distance information included + +**Key Features:** +- Reuses existing `calculateDistance()` method for consistency +- Handles clubs without GPS coordinates gracefully +- Returns `null` for distance when location is not provided +- Maintains backward compatibility (works with or without location parameters) + +### 2. Frontend Changes - `resources/views/clubs/explore.blade.php` + +#### Modified `fetchAllClubs()` Function +- **Location Integration**: Now passes user location (latitude/longitude) as query parameters when available +- **Dynamic URL Building**: Constructs URL with location parameters if `userLocation` is set +- **Fallback**: Works without location parameters if user location is unavailable + +#### Modified Category Button Logic +- **Updated Behavior**: Both "All" and "Clubs" (sports-clubs) tabs now use `fetchAllClubs()` +- **Consistent Sorting**: Ensures location-based sorting is applied for both categories +- **Other Categories**: Other categories continue to use `fetchNearbyClubs()` as before + +## How It Works + +1. **User Location Detection**: + - Page automatically detects user's location on load + - Location is stored in `userLocation` variable + +2. **Tab Selection**: + - When "All" or "Clubs" tab is clicked, `fetchAllClubs()` is called + - If user location is available, it's sent to the backend + +3. **Backend Processing**: + - Backend receives location parameters + - Calculates distance for each club with GPS coordinates + - Sorts clubs by distance (nearest to farthest) + - Returns sorted list with distance information + +4. **Display**: + - Clubs are displayed in cards with distance shown + - Card style matches the exact design specification + - Distance is displayed as "X.XX km away" + +## Card Style +The card implementation maintains the exact style as specified: +- Club image with logo overlay (bottom-right corner) +- "Sports Club" badge (top-right corner) +- Club name (bold, large text) +- Distance with navigation icon (red text) +- Owner/address information +- Stats grid (Members, Packages, Trainers) +- Action buttons (Join Club, View Details) + +## Testing + +To test the implementation: + +1. Visit `http://localhost:8000/explore` +2. Allow location access when prompted +3. Click on "All" tab - clubs should be sorted by distance +4. Click on "Clubs" tab - clubs should be sorted by distance +5. Verify distance is displayed correctly on each card +6. Verify card style matches the design specification + +## Technical Details + +### Distance Calculation +- Uses Haversine formula for accurate distance calculation +- Returns distance in kilometers +- Rounded to 2 decimal places for display + +### Sorting Algorithm +```php +// Clubs with distance come first (sorted by distance) +// Clubs without distance come last (maintain original order) +if (both have distance) -> sort by distance +if (only one has distance) -> prioritize it +if (neither has distance) -> maintain order +``` + +### API Endpoints Used +- `GET /clubs/all?latitude={lat}&longitude={lng}` - Fetch all clubs with distance sorting +- `GET /clubs/nearby?latitude={lat}&longitude={lng}&radius={km}` - Fetch nearby clubs within radius + +## Files Modified +1. `app/Http/Controllers/ClubController.php` - Backend logic +2. `resources/views/clubs/explore.blade.php` - Frontend JavaScript (also fixed missing `pageMap` variable declaration bug) + +## Bug Fixes +During implementation, discovered and fixed an existing bug: +- **Missing Variable Declaration**: Added `let pageMap;` declaration that was causing JavaScript errors + +## Backward Compatibility +- All changes are backward compatible +- Works with or without location parameters +- Existing functionality remains intact +- No database schema changes required + +## Future Enhancements +Potential improvements for future iterations: +- Add distance unit toggle (km/miles) +- Add custom radius filter for "All" tab +- Cache distance calculations for performance +- Add map view with sorted markers diff --git a/TESTING_ADMIN_PANEL.md b/TESTING_ADMIN_PANEL.md new file mode 100644 index 0000000..fc8d56a --- /dev/null +++ b/TESTING_ADMIN_PANEL.md @@ -0,0 +1,425 @@ +# Admin Panel Testing Guide + +## Pre-Testing Setup + +### Step 1: Assign Super Admin Role + +Run this command with your email: + +```bash +php artisan admin:make-super your-email@example.com +``` + +### Step 2: Start the Development Server + +```bash +php artisan serve +``` + +The server should start at: `http://localhost:8000` + +--- + +## Testing Checklist + +### ✅ Authentication & Authorization + +- [ ] **Login as regular user** + - Navigate to `/admin` + - Should see 403 Forbidden error + +- [ ] **Login as super admin** + - Navigate to `/admin` + - Should see admin dashboard + +- [ ] **Test middleware protection** + - Logout + - Try to access `/admin` directly + - Should redirect to login page + +--- + +### ✅ Platform Dashboard (`/admin`) + +- [ ] **Page loads successfully** + - No errors in browser console + - All stat cards display correctly + +- [ ] **Statistics display** + - Total Clubs count + - Total Members count + - Active Clubs count + - Total Revenue (BHD) + +- [ ] **Quick action cards** + - "Manage Clubs" button works + - "Manage Members" button works + - "Database Backup" button works + +- [ ] **Sidebar navigation** + - All menu items visible + - Active state highlights current page + - Icons display correctly + +--- + +### ✅ All Clubs Management (`/admin/clubs`) + +- [ ] **Page loads successfully** + - Grid layout displays + - Search bar visible + - "Add New Club" button visible + +- [ ] **Empty state** (if no clubs exist) + - Friendly message displays + - "Add New Club" button works + +- [ ] **Club cards display** (if clubs exist) + - Cover image or placeholder + - Club logo or initial + - Club name + - Address (if available) + - Stats (members, packages, trainers) + - Owner information + - Edit and Delete buttons + +- [ ] **Search functionality** + - Enter search term + - Results filter correctly + - Clear search button works + +- [ ] **Pagination** + - Multiple pages display if >12 clubs + - Page navigation works + +--- + +### ✅ Create Club (`/admin/clubs/create`) + +- [ ] **Form loads successfully** + - All fields visible + - Owner dropdown populated + - Default values set (currency, timezone, country) + +- [ ] **Auto-slug generation** + - Type in club name + - Slug field auto-fills + - Special characters converted to hyphens + +- [ ] **Form validation** + - Submit empty form + - Required field errors display + - Invalid email shows error + - Invalid GPS coordinates show error + +- [ ] **File uploads** + - Select logo image + - Select cover image + - File size validation (max 2MB) + +- [ ] **Successful submission** + - Fill all required fields + - Submit form + - Redirects to clubs list + - Success message displays + - New club appears in list + +- [ ] **Cancel button** + - Click cancel + - Returns to clubs list + - No data saved + +--- + +### ✅ Edit Club (`/admin/clubs/{id}/edit`) + +- [ ] **Form loads with existing data** + - All fields pre-filled + - Current logo displays (if exists) + - Current cover image displays (if exists) + +- [ ] **Update basic information** + - Change club name + - Change slug + - Submit form + - Changes saved successfully + +- [ ] **Update contact information** + - Change email + - Change phone + - Change currency/timezone/country + - Submit and verify changes + +- [ ] **Update location** + - Change address + - Change GPS coordinates + - Submit and verify changes + +- [ ] **Replace images** + - Upload new logo + - Upload new cover image + - Old images deleted + - New images display + +- [ ] **Form validation** + - Enter invalid data + - Errors display correctly + +- [ ] **Cancel button** + - Click cancel + - Returns to clubs list + - No changes saved + +--- + +### ✅ Delete Club + +- [ ] **Delete confirmation** + - Click delete button on club card + - Confirmation dialog appears + - Warning message clear + +- [ ] **Cancel deletion** + - Click cancel in dialog + - Club not deleted + - Remains in list + +- [ ] **Confirm deletion** + - Click delete button + - Confirm in dialog + - Club deleted successfully + - Success message displays + - Club removed from list + - Associated files deleted + +--- + +### ✅ All Members Management (`/admin/members`) + +- [ ] **Page loads successfully** + - Grid layout displays + - Search bar visible + +- [ ] **Empty state** (if no members) + - Friendly message displays + +- [ ] **Member cards display** (if members exist) + - Avatar or initial + - Full name + - Adult/Child badge + - Club count badge + - Contact information + - Gender, age, nationality + - Horoscope and birthday + - Member since date + - View and Edit buttons + +- [ ] **Search functionality** + - Search by name + - Search by phone + - Search by nationality + - Results filter correctly + +- [ ] **Pagination** + - Multiple pages if >20 members + - Navigation works + +- [ ] **View member** + - Click "View" button + - Redirects to member profile + +- [ ] **Edit member** + - Click "Edit" button + - Redirects to edit form + +--- + +### ✅ Database Backup (`/admin/backup`) + +- [ ] **Page loads successfully** + - Three operation cards display + - Warning message visible + - Best practices section visible + +- [ ] **Download Backup** + - Click "Download Full Backup" + - Confirmation dialog appears + - JSON file downloads + - File name includes timestamp + - File contains all tables + +- [ ] **Restore Database** + - Click "Restore from Backup" + - Modal opens + - Warning messages display + - File input accepts only JSON + - Checkbox required + - Cancel button works + +- [ ] **Restore functionality** (⚠️ TEST IN STAGING ONLY) + - Upload valid backup JSON + - Check confirmation checkbox + - Submit form + - Final confirmation dialog + - Database restored successfully + - Success message displays + +- [ ] **Export Auth Users** + - Click "Export Users" + - JSON file downloads + - Contains user data with encrypted passwords + +--- + +### ✅ UI/UX Elements + +- [ ] **Sidebar navigation** + - Fixed position on scroll + - Active state highlights + - All links work + - "Back to Explore" link works + +- [ ] **Top navbar** + - User name displays + - Dropdown menu works + - Profile link works + - Logout works + +- [ ] **Alert messages** + - Success messages display (green) + - Error messages display (red) + - Dismissible with X button + - Auto-dismiss after 5 seconds (optional) + +- [ ] **Responsive design** + - Test on mobile (< 768px) + - Sidebar collapses + - Cards stack vertically + - Forms remain usable + - Tables scroll horizontally + +- [ ] **Loading states** + - Forms disable on submit + - Loading indicators show (if implemented) + +- [ ] **Empty states** + - Friendly messages + - Helpful icons + - Call-to-action buttons + +--- + +### ✅ Performance & Security + +- [ ] **Page load times** + - Dashboard loads < 2 seconds + - Clubs list loads < 3 seconds + - Members list loads < 3 seconds + +- [ ] **Database queries** + - Check Laravel Debugbar (if installed) + - No N+1 query problems + - Eager loading used + +- [ ] **CSRF protection** + - All forms have @csrf token + - Forms fail without token + +- [ ] **File upload security** + - Only images accepted + - File size limits enforced + - Files stored securely + +- [ ] **SQL injection prevention** + - Try SQL in search fields + - No errors or data leaks + +- [ ] **XSS prevention** + - Try JavaScript in text fields + - Scripts not executed + +--- + +## Browser Compatibility + +Test in multiple browsers: + +- [ ] Chrome/Edge (Chromium) +- [ ] Firefox +- [ ] Safari (if on Mac) + +--- + +## Common Issues & Solutions + +### Issue: 403 Forbidden on /admin + +**Solution:** +```bash +php artisan admin:make-super your-email@example.com +``` +Then logout and login again. + +### Issue: Role not found + +**Solution:** +```bash +php artisan db:seed --class=RolePermissionSeeder +``` + +### Issue: Images not displaying + +**Solution:** +```bash +php artisan storage:link +``` + +### Issue: Validation errors not showing + +**Check:** +- @error directives in blade files +- Form has @csrf token +- Input names match validation rules + +--- + +## Test Data Creation + +### Create Test Club via Tinker: + +```bash +php artisan tinker +``` + +```php +$user = User::first(); +$club = Tenant::create([ + 'owner_user_id' => $user->id, + 'club_name' => 'Test Taekwondo Club', + 'slug' => 'test-taekwondo', + 'email' => 'test@club.com', + 'currency' => 'BHD', + 'timezone' => 'Asia/Bahrain', + 'country' => 'BH', + 'address' => 'Test Address, Manama', + 'gps_lat' => 26.0667, + 'gps_long' => 50.5577, +]); +``` + +--- + +## Reporting Issues + +When reporting issues, include: + +1. **Steps to reproduce** +2. **Expected behavior** +3. **Actual behavior** +4. **Browser and version** +5. **Screenshots** (if applicable) +6. **Error messages** (from browser console or Laravel log) + +--- + +**Happy Testing! 🚀** diff --git a/TODO_ADMIN_PANEL.md b/TODO_ADMIN_PANEL.md new file mode 100644 index 0000000..5b9f077 --- /dev/null +++ b/TODO_ADMIN_PANEL.md @@ -0,0 +1,371 @@ +# TAKEONE Admin Panel Implementation TODO + +## Project Overview +Building a comprehensive Laravel-based admin panel for multi-club sports management platform. + +**Tech Stack:** Laravel 12, Blade Templates, Bootstrap 5, Tailwind CSS, PostgreSQL/MySQL +**Start Date:** 2026-01-25 +**Status:** In Progress + +--- + +## Phase 1: Database Schema Expansion ⏳ + +### Core Admin Tables +- [ ] Create roles and permissions tables migration +- [ ] Expand tenants (clubs) table with all required fields +- [ ] Create club facilities table +- [ ] Create club instructors table +- [ ] Create club activities table +- [ ] Create club packages table +- [ ] Create club package activities (pivot) table +- [ ] Create club member subscriptions table +- [ ] Create club transactions table +- [ ] Create club gallery images table +- [ ] Create club social links table +- [ ] Create club bank accounts table (encrypted) +- [ ] Create club messages table +- [ ] Create club reviews table +- [ ] Create club settings table + +--- + +## Phase 2: Models & Relationships + +### Eloquent Models +- [ ] Role model +- [ ] Permission model +- [ ] ClubFacility model +- [ ] ClubInstructor model +- [ ] ClubActivity model +- [ ] ClubPackage model +- [ ] ClubMemberSubscription model +- [ ] ClubTransaction model +- [ ] ClubGalleryImage model +- [ ] ClubSocialLink model +- [ ] ClubBankAccount model +- [ ] ClubMessage model +- [ ] ClubReview model +- [ ] ClubSettings model +- [ ] Update Tenant model with new relationships +- [ ] Update User model with admin relationships + +--- + +## Phase 3: Role-Based Access Control (RBAC) + +### Authorization System +- [ ] Create role middleware +- [ ] Create permission middleware +- [ ] Create policy classes for all models +- [ ] Create role seeder (Super Admin, Club Admin, Instructor, Member) +- [ ] Create permission seeder +- [ ] Add helper functions for role checking +- [ ] Update User model with role methods + +--- + +## Phase 4: Platform-Level Admin (PRIORITY) + +### Routes +- [ ] Platform admin routes group +- [ ] All clubs management routes +- [ ] All members management routes +- [ ] Database backup/restore routes + +### Controllers +- [ ] Admin\PlatformController + - [ ] Dashboard overview + - [ ] Statistics and analytics +- [ ] Admin\ClubManagementController + - [ ] Index (all clubs grid) + - [ ] Create new club + - [ ] Edit club + - [ ] Delete club + - [ ] Search/filter clubs +- [ ] Admin\MemberManagementController + - [ ] Index (all members grid) + - [ ] Create member + - [ ] Edit member + - [ ] Delete member + - [ ] Search/filter members + - [ ] Add child member +- [ ] Admin\BackupController + - [ ] Download database backup (JSON) + - [ ] Restore database from backup + - [ ] Export auth users + +### Views (Blade Templates) +- [ ] layouts/admin.blade.php (platform admin layout) +- [ ] admin/dashboard.blade.php +- [ ] admin/clubs/index.blade.php (grid view) +- [ ] admin/clubs/create.blade.php +- [ ] admin/clubs/edit.blade.php +- [ ] admin/members/index.blade.php (grid view) +- [ ] admin/members/create.blade.php +- [ ] admin/members/edit.blade.php +- [ ] admin/backup/index.blade.php + +--- + +## Phase 5: Club-Level Admin Dashboard + +### Routes +- [ ] Club admin routes group (/club/{slug}/admin) +- [ ] Dashboard routes +- [ ] Club details routes (multi-tab) +- [ ] Gallery routes +- [ ] Facilities routes +- [ ] Instructors routes +- [ ] Activities routes +- [ ] Packages routes +- [ ] Members routes +- [ ] Financials routes +- [ ] Messages routes +- [ ] Analytics routes + +### Controllers +- [ ] Club\DashboardController +- [ ] Club\ClubDetailsController +- [ ] Club\GalleryController +- [ ] Club\FacilityController +- [ ] Club\InstructorController +- [ ] Club\ActivityController +- [ ] Club\PackageController +- [ ] Club\MemberController +- [ ] Club\FinancialController +- [ ] Club\MessageController +- [ ] Club\AnalyticsController + +### Views (Blade Templates) +- [ ] layouts/club-admin.blade.php (with sidebar) +- [ ] club/admin/dashboard.blade.php +- [ ] club/admin/details/index.blade.php (tabs) +- [ ] club/admin/gallery/index.blade.php +- [ ] club/admin/facilities/index.blade.php +- [ ] club/admin/instructors/index.blade.php +- [ ] club/admin/activities/index.blade.php +- [ ] club/admin/packages/index.blade.php +- [ ] club/admin/members/index.blade.php +- [ ] club/admin/financials/index.blade.php +- [ ] club/admin/messages/index.blade.php +- [ ] club/admin/analytics/index.blade.php + +--- + +## Phase 6: Core Features Implementation + +### Multi-Currency Support +- [ ] Create currency dropdown component +- [ ] Add currency helper functions +- [ ] Store currency in club settings +- [ ] Display prices in club currency + +### Multi-Timezone Support +- [ ] Create timezone dropdown component +- [ ] Add timezone helper functions +- [ ] Store timezone in club settings +- [ ] Display times in club timezone + +### File Upload & Management +- [ ] Configure storage (S3 or local) +- [ ] Image optimization on upload +- [ ] Gallery management CRUD +- [ ] File deletion handling + +### Financial System +- [ ] Transaction ledger view +- [ ] Income tracking +- [ ] Expense tracking +- [ ] Refund handling +- [ ] Financial reports +- [ ] CSV export functionality +- [ ] Charts (Chart.js integration) + +### Backup & Restore +- [ ] JSON export of all tables +- [ ] Secure backup storage +- [ ] Restore functionality +- [ ] Validation on restore +- [ ] Export auth users with encrypted passwords + +### Analytics Dashboard +- [ ] Member growth chart +- [ ] Revenue trends chart +- [ ] Package popularity metrics +- [ ] Activity attendance rates +- [ ] Instructor performance metrics +- [ ] Member retention rates +- [ ] Financial health indicators + +### Messaging System +- [ ] Inbox/conversation list +- [ ] Message thread view +- [ ] Send message functionality +- [ ] Mark as read/unread +- [ ] Notification integration + +--- + +## Phase 7: Additional Features + +### Club Details Management +- [ ] Basic information tab +- [ ] Contact information tab +- [ ] Location & GPS tab (with map) +- [ ] Branding assets tab +- [ ] Social media links tab +- [ ] Banking tab (encrypted) +- [ ] Settings tab (code prefixes) +- [ ] Danger zone (delete club) + +### Gallery Management +- [ ] Grid display of images +- [ ] Multiple image upload +- [ ] Lightbox view +- [ ] Delete images +- [ ] Image captions + +### Facilities Management +- [ ] Card-based layout +- [ ] Add facility +- [ ] Edit facility +- [ ] Delete facility +- [ ] GPS location on map +- [ ] Availability status + +### Instructors Management +- [ ] Card layout +- [ ] Add instructor +- [ ] Edit instructor +- [ ] Delete instructor +- [ ] Skills/specialties tags +- [ ] Rating system +- [ ] Search/filter + +### Activities Management +- [ ] Grid of activity cards +- [ ] Add activity +- [ ] Edit activity +- [ ] Delete activity +- [ ] Duplicate activity +- [ ] Schedule management +- [ ] Facility assignment + +### Packages Management +- [ ] Large card layout +- [ ] Add package +- [ ] Edit package +- [ ] Delete package +- [ ] Duplicate package +- [ ] Single/Multi activity types +- [ ] Age range configuration +- [ ] Price management +- [ ] Activity inclusion + +### Members Management +- [ ] Current members tab +- [ ] Pending requests tab +- [ ] Status filters +- [ ] Add existing user +- [ ] Walk-in registration +- [ ] Member invitation system +- [ ] Subscription management + +--- + +## Phase 8: Components & Reusables + +### Blade Components +- [ ] Country dropdown component +- [ ] Currency dropdown component +- [ ] Timezone dropdown component +- [ ] Phone code dropdown component +- [ ] Nationality dropdown component +- [ ] Image upload modal component +- [ ] Confirmation modal component +- [ ] Stats card component +- [ ] Chart component wrapper + +--- + +## Phase 9: Testing & Quality Assurance + +### Tests +- [ ] Feature tests for platform admin +- [ ] Feature tests for club admin +- [ ] Policy tests +- [ ] Model tests +- [ ] Controller tests +- [ ] Validation tests + +### Seeders +- [ ] Role and permission seeder +- [ ] Demo clubs seeder +- [ ] Demo members seeder +- [ ] Demo packages seeder +- [ ] Demo transactions seeder + +### Code Quality +- [ ] Run Laravel Pint (code formatting) +- [ ] Security audit (CSRF, XSS, SQL injection) +- [ ] Performance optimization (caching, eager loading) +- [ ] Database indexing +- [ ] Query optimization + +--- + +## Phase 10: Documentation & Deployment + +### Documentation +- [ ] API documentation +- [ ] Admin user guide +- [ ] Installation guide +- [ ] Deployment guide +- [ ] Database schema documentation + +### Deployment Preparation +- [ ] Environment configuration +- [ ] Storage setup +- [ ] Backup automation +- [ ] SSL certificates +- [ ] Performance monitoring +- [ ] Error logging + +--- + +## Current Progress + +**Phase 1:** Not Started +**Phase 2:** Not Started +**Phase 3:** Not Started +**Phase 4:** Not Started (PRIORITY) +**Overall:** 0% Complete + +--- + +## Notes + +- Using Blade templates (not React) for consistency with existing codebase +- Bootstrap 5 + custom soft purple theme +- All club-specific tables have tenant_id foreign key +- Soft deletes on important records +- Encrypted storage for bank account information +- Multi-currency support with BHD as default +- Multi-timezone handling +- File upload with image optimization +- Role-based access control throughout + +--- + +## Next Steps + +1. Create all database migrations (Phase 1) +2. Create all Eloquent models (Phase 2) +3. Implement RBAC system (Phase 3) +4. Build platform-level admin (Phase 4) - PRIORITY +5. Continue with remaining phases + +--- + +**Last Updated:** 2026-01-25 diff --git a/app/Console/Commands/MakeSuperAdmin.php b/app/Console/Commands/MakeSuperAdmin.php new file mode 100644 index 0000000..d95533b --- /dev/null +++ b/app/Console/Commands/MakeSuperAdmin.php @@ -0,0 +1,53 @@ +argument('email'); + + $user = User::where('email', $email)->first(); + + if (!$user) { + $this->error("User with email '{$email}' not found!"); + return 1; + } + + // Check if user already has super-admin role + if ($user->hasRole('super-admin')) { + $this->info("User '{$user->full_name}' ({$email}) is already a super admin!"); + return 0; + } + + // Assign super-admin role + $user->assignRole('super-admin'); + + $this->info("✅ Successfully made '{$user->full_name}' ({$email}) a super admin!"); + $this->info("They can now access the admin panel at: " . url('/admin')); + + return 0; + } +} diff --git a/app/Http/Controllers/Admin/PlatformController.php b/app/Http/Controllers/Admin/PlatformController.php new file mode 100644 index 0000000..4e0d419 --- /dev/null +++ b/app/Http/Controllers/Admin/PlatformController.php @@ -0,0 +1,338 @@ +withCount(['members', 'packages', 'instructors']) + ->latest() + ->get(); + + $clubsCount = $clubs->count(); + + return view('admin.platform.index', compact('clubs', 'clubsCount')); + } + + /** + * Display all clubs management page. + */ + public function clubs(Request $request) + { + $search = $request->input('search'); + + $clubs = Tenant::with(['owner', 'members']) + ->withCount(['members', 'packages', 'instructors']) + ->when($search, function ($query, $search) { + $query->where('club_name', 'like', "%{$search}%") + ->orWhere('address', 'like', "%{$search}%") + ->orWhere('description', 'like', "%{$search}%"); + }) + ->latest() + ->paginate(12); + + return view('admin.platform.clubs', compact('clubs', 'search')); + } + + /** + * Display all members management page. + */ + public function members(Request $request) + { + $search = $request->input('search'); + + $members = User::with(['memberClubs', 'dependents']) + ->withCount('memberClubs') + ->when($search, function ($query, $search) { + $query->where('full_name', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%") + ->orWhere('nationality', 'like', "%{$search}%") + ->orWhereRaw("JSON_EXTRACT(mobile, '$.number') LIKE ?", ["%{$search}%"]); + }) + ->latest() + ->paginate(20); + + return view('admin.platform.members', compact('members', 'search')); + } + + /** + * Show create club form. + */ + public function createClub() + { + $users = User::orderBy('full_name')->get(); + return view('admin.platform.create-club', compact('users')); + } + + /** + * Store a new club. + */ + public function storeClub(Request $request) + { + $validated = $request->validate([ + 'owner_user_id' => 'required|exists:users,id', + 'club_name' => 'required|string|max:255', + 'slug' => 'required|string|max:255|unique:tenants,slug', + 'email' => 'nullable|email', + 'phone_code' => 'nullable|string', + 'phone_number' => 'nullable|string', + 'currency' => 'nullable|string|max:3', + 'timezone' => 'nullable|string', + 'country' => 'nullable|string', + 'address' => 'nullable|string', + 'gps_lat' => 'nullable|numeric|between:-90,90', + 'gps_long' => 'nullable|numeric|between:-180,180', + 'logo' => 'nullable|image|max:2048', + 'cover_image' => 'nullable|image|max:2048', + ]); + + // Handle phone as JSON + if ($request->filled('phone_code') && $request->filled('phone_number')) { + $validated['phone'] = [ + 'code' => $request->phone_code, + 'number' => $request->phone_number, + ]; + } + + // Handle logo upload + if ($request->hasFile('logo')) { + $validated['logo'] = $request->file('logo')->store('clubs/logos', 'public'); + } + + // Handle cover image upload + if ($request->hasFile('cover_image')) { + $validated['cover_image'] = $request->file('cover_image')->store('clubs/covers', 'public'); + } + + $club = Tenant::create($validated); + + // Assign club-admin role to owner + $owner = User::find($validated['owner_user_id']); + $owner->assignRole('club-admin', $club->id); + + return redirect()->route('admin.platform.clubs') + ->with('success', 'Club created successfully!'); + } + + /** + * Show edit club form. + */ + public function editClub(Tenant $club) + { + $users = User::orderBy('full_name')->get(); + return view('admin.platform.edit-club', compact('club', 'users')); + } + + /** + * Update a club. + */ + public function updateClub(Request $request, Tenant $club) + { + $validated = $request->validate([ + 'owner_user_id' => 'required|exists:users,id', + 'club_name' => 'required|string|max:255', + 'slug' => 'required|string|max:255|unique:tenants,slug,' . $club->id, + 'email' => 'nullable|email', + 'phone_code' => 'nullable|string', + 'phone_number' => 'nullable|string', + 'currency' => 'nullable|string|max:3', + 'timezone' => 'nullable|string', + 'country' => 'nullable|string', + 'address' => 'nullable|string', + 'gps_lat' => 'nullable|numeric|between:-90,90', + 'gps_long' => 'nullable|numeric|between:-180,180', + 'logo' => 'nullable|image|max:2048', + 'cover_image' => 'nullable|image|max:2048', + ]); + + // Handle phone as JSON + if ($request->filled('phone_code') && $request->filled('phone_number')) { + $validated['phone'] = [ + 'code' => $request->phone_code, + 'number' => $request->phone_number, + ]; + } + + // Handle logo upload + if ($request->hasFile('logo')) { + // Delete old logo + if ($club->logo) { + Storage::disk('public')->delete($club->logo); + } + $validated['logo'] = $request->file('logo')->store('clubs/logos', 'public'); + } + + // Handle cover image upload + if ($request->hasFile('cover_image')) { + // Delete old cover + if ($club->cover_image) { + Storage::disk('public')->delete($club->cover_image); + } + $validated['cover_image'] = $request->file('cover_image')->store('clubs/covers', 'public'); + } + + $club->update($validated); + + return redirect()->route('admin.platform.clubs') + ->with('success', 'Club updated successfully!'); + } + + /** + * Delete a club. + */ + public function destroyClub(Tenant $club) + { + // Delete associated files + if ($club->logo) { + Storage::disk('public')->delete($club->logo); + } + if ($club->cover_image) { + Storage::disk('public')->delete($club->cover_image); + } + if ($club->favicon) { + Storage::disk('public')->delete($club->favicon); + } + + // Delete club (cascade will handle related records) + $club->delete(); + + return redirect()->route('admin.platform.clubs') + ->with('success', 'Club deleted successfully!'); + } + + /** + * Display database backup page. + */ + public function backup() + { + return view('admin.platform.backup'); + } + + /** + * Download database backup as JSON. + */ + public function downloadBackup() + { + $tables = [ + 'users', + 'user_relationships', + 'tenants', + 'memberships', + 'invoices', + 'roles', + 'permissions', + 'role_permission', + 'user_roles', + 'club_facilities', + 'club_instructors', + 'club_activities', + 'club_packages', + 'club_package_activities', + 'club_member_subscriptions', + 'club_transactions', + 'club_gallery_images', + 'club_social_links', + 'club_bank_accounts', + 'club_messages', + 'club_reviews', + 'health_records', + 'tournament_events', + 'performance_results', + 'goals', + 'attendance_records', + 'club_affiliations', + 'skill_acquisitions', + ]; + + $backup = []; + foreach ($tables as $table) { + $backup[$table] = DB::table($table)->get()->toArray(); + } + + $filename = 'takeone_backup_' . date('Y-m-d_H-i-s') . '.json'; + + return response()->json($backup, 200, [ + 'Content-Type' => 'application/json', + 'Content-Disposition' => 'attachment; filename="' . $filename . '"', + ]); + } + + /** + * Restore database from JSON backup. + */ + public function restoreBackup(Request $request) + { + $request->validate([ + 'backup_file' => 'required|file|mimes:json', + ]); + + $file = $request->file('backup_file'); + $content = file_get_contents($file->getRealPath()); + $backup = json_decode($content, true); + + if (!$backup) { + return back()->with('error', 'Invalid backup file format.'); + } + + DB::beginTransaction(); + try { + // Disable foreign key checks + DB::statement('SET FOREIGN_KEY_CHECKS=0'); + + foreach ($backup as $table => $records) { + // Truncate table + DB::table($table)->truncate(); + + // Insert records in chunks + if (!empty($records)) { + $chunks = array_chunk($records, 100); + foreach ($chunks as $chunk) { + DB::table($table)->insert($chunk); + } + } + } + + // Re-enable foreign key checks + DB::statement('SET FOREIGN_KEY_CHECKS=1'); + + DB::commit(); + + return back()->with('success', 'Database restored successfully!'); + } catch (\Exception $e) { + DB::rollBack(); + DB::statement('SET FOREIGN_KEY_CHECKS=1'); + + return back()->with('error', 'Restore failed: ' . $e->getMessage()); + } + } + + /** + * Export all authentication users. + */ + public function exportAuthUsers() + { + $users = User::select('id', 'full_name', 'email', 'password', 'created_at') + ->get() + ->toArray(); + + $filename = 'auth_users_' . date('Y-m-d_H-i-s') . '.json'; + + return response()->json($users, 200, [ + 'Content-Type' => 'application/json', + 'Content-Disposition' => 'attachment; filename="' . $filename . '"', + ]); + } +} diff --git a/app/Http/Controllers/ClubController.php b/app/Http/Controllers/ClubController.php index 62add1b..d6ffb3b 100644 --- a/app/Http/Controllers/ClubController.php +++ b/app/Http/Controllers/ClubController.php @@ -104,12 +104,30 @@ class ClubController extends Controller /** * Get all clubs for the map view. + * If latitude and longitude are provided, calculate distance and sort by nearest. */ - public function all() + public function all(Request $request) { - $clubs = Tenant::with('owner') - ->get() - ->map(function ($club) { + $userLat = $request->input('latitude'); + $userLng = $request->input('longitude'); + + $clubs = Tenant::with('owner')->get(); + + // If user location is provided, calculate distance for each club + if ($userLat !== null && $userLng !== null) { + $clubsWithDistance = $clubs->map(function ($club) use ($userLat, $userLng) { + $distance = null; + + // Calculate distance only if club has GPS coordinates + if ($club->gps_lat && $club->gps_long) { + $distance = $this->calculateDistance( + $userLat, + $userLng, + $club->gps_lat, + $club->gps_long + ); + } + return [ 'id' => $club->id, 'club_name' => $club->club_name, @@ -117,14 +135,57 @@ class ClubController extends Controller 'logo' => $club->logo, 'gps_lat' => $club->gps_lat ? (float) $club->gps_lat : null, 'gps_long' => $club->gps_long ? (float) $club->gps_long : null, + 'distance' => $distance !== null ? round($distance, 2) : null, 'owner_name' => $club->owner ? $club->owner->full_name : 'N/A', ]; }); + // Sort by distance (nearest first), clubs without GPS coordinates go to the end + $clubsWithDistance = $clubsWithDistance->sort(function ($a, $b) { + // If both have distance, sort by distance + if ($a['distance'] !== null && $b['distance'] !== null) { + return $a['distance'] <=> $b['distance']; + } + // If only one has distance, prioritize it + if ($a['distance'] !== null) { + return -1; + } + if ($b['distance'] !== null) { + return 1; + } + // If neither has distance, maintain original order + return 0; + })->values(); + + return response()->json([ + 'success' => true, + 'clubs' => $clubsWithDistance, + 'total' => $clubsWithDistance->count(), + 'user_location' => [ + 'latitude' => $userLat, + 'longitude' => $userLng, + ], + ]); + } + + // If no location provided, return clubs without distance calculation + $clubsData = $clubs->map(function ($club) { + return [ + 'id' => $club->id, + 'club_name' => $club->club_name, + 'slug' => $club->slug, + 'logo' => $club->logo, + 'gps_lat' => $club->gps_lat ? (float) $club->gps_lat : null, + 'gps_long' => $club->gps_long ? (float) $club->gps_long : null, + 'distance' => null, + 'owner_name' => $club->owner ? $club->owner->full_name : 'N/A', + ]; + }); + return response()->json([ 'success' => true, - 'clubs' => $clubs, - 'total' => $clubs->count(), + 'clubs' => $clubsData, + 'total' => $clubsData->count(), ]); } } diff --git a/app/Http/Middleware/CheckPermission.php b/app/Http/Middleware/CheckPermission.php new file mode 100644 index 0000000..e26d77d --- /dev/null +++ b/app/Http/Middleware/CheckPermission.php @@ -0,0 +1,35 @@ +check()) { + return redirect()->route('login'); + } + + $user = auth()->user(); + $tenantId = $request->route('tenant') ?? $request->route('slug') + ? \App\Models\Tenant::where('slug', $request->route('slug'))->value('id') + : null; + + // Check if user has the required permission + if ($user->hasPermission($permission, $tenantId)) { + return $next($request); + } + + // If user doesn't have required permission, abort with 403 + abort(403, 'Unauthorized action.'); + } +} diff --git a/app/Http/Middleware/CheckRole.php b/app/Http/Middleware/CheckRole.php new file mode 100644 index 0000000..168d571 --- /dev/null +++ b/app/Http/Middleware/CheckRole.php @@ -0,0 +1,35 @@ +check()) { + return redirect()->route('login'); + } + + $user = auth()->user(); + $tenantId = $request->route('tenant') ?? $request->route('slug') + ? \App\Models\Tenant::where('slug', $request->route('slug'))->value('id') + : null; + + // Check if user has any of the required roles + if ($user->hasAnyRole($roles, $tenantId)) { + return $next($request); + } + + // If user doesn't have required role, abort with 403 + abort(403, 'Unauthorized action.'); + } +} diff --git a/app/Models/ClubActivity.php b/app/Models/ClubActivity.php new file mode 100644 index 0000000..9ffc916 --- /dev/null +++ b/app/Models/ClubActivity.php @@ -0,0 +1,72 @@ + + */ + protected $fillable = [ + 'tenant_id', + 'name', + 'duration_minutes', + 'frequency_per_week', + 'facility_id', + 'schedule', + 'description', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'duration_minutes' => 'integer', + 'frequency_per_week' => 'integer', + 'schedule' => 'array', + ]; + + /** + * Get the club that owns the activity. + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } + + /** + * Get the facility where the activity takes place. + */ + public function facility(): BelongsTo + { + return $this->belongsTo(ClubFacility::class, 'facility_id'); + } + + /** + * Get the packages that include this activity. + */ + public function packages(): BelongsToMany + { + return $this->belongsToMany(ClubPackage::class, 'club_package_activities', 'activity_id', 'package_id') + ->withPivot('instructor_id') + ->withTimestamps(); + } +} diff --git a/app/Models/ClubBankAccount.php b/app/Models/ClubBankAccount.php new file mode 100644 index 0000000..458aaa9 --- /dev/null +++ b/app/Models/ClubBankAccount.php @@ -0,0 +1,104 @@ + + */ + protected $fillable = [ + 'tenant_id', + 'bank_name', + 'account_name', + 'account_number', + 'iban', + 'swift_code', + 'is_primary', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'is_primary' => 'boolean', + ]; + + /** + * Get the club that owns the bank account. + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } + + /** + * Encrypt and decrypt account_number. + */ + protected function accountNumber(): Attribute + { + return Attribute::make( + get: fn ($value) => $value ? Crypt::decryptString($value) : null, + set: fn ($value) => $value ? Crypt::encryptString($value) : null, + ); + } + + /** + * Encrypt and decrypt iban. + */ + protected function iban(): Attribute + { + return Attribute::make( + get: fn ($value) => $value ? Crypt::decryptString($value) : null, + set: fn ($value) => $value ? Crypt::encryptString($value) : null, + ); + } + + /** + * Encrypt and decrypt swift_code. + */ + protected function swiftCode(): Attribute + { + return Attribute::make( + get: fn ($value) => $value ? Crypt::decryptString($value) : null, + set: fn ($value) => $value ? Crypt::encryptString($value) : null, + ); + } + + /** + * Get masked account number for display. + */ + public function getMaskedAccountNumber(): string + { + $accountNumber = $this->account_number; + if (!$accountNumber) { + return 'N/A'; + } + + $length = strlen($accountNumber); + if ($length <= 4) { + return str_repeat('*', $length); + } + + return str_repeat('*', $length - 4) . substr($accountNumber, -4); + } +} diff --git a/app/Models/ClubFacility.php b/app/Models/ClubFacility.php new file mode 100644 index 0000000..a10f609 --- /dev/null +++ b/app/Models/ClubFacility.php @@ -0,0 +1,62 @@ + + */ + protected $fillable = [ + 'tenant_id', + 'name', + 'photo', + 'address', + 'gps_lat', + 'gps_long', + 'is_available', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'gps_lat' => 'decimal:7', + 'gps_long' => 'decimal:7', + 'is_available' => 'boolean', + ]; + + /** + * Get the club that owns the facility. + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } + + /** + * Get the activities for the facility. + */ + public function activities(): HasMany + { + return $this->hasMany(ClubActivity::class, 'facility_id'); + } +} diff --git a/app/Models/ClubGalleryImage.php b/app/Models/ClubGalleryImage.php new file mode 100644 index 0000000..2773cd0 --- /dev/null +++ b/app/Models/ClubGalleryImage.php @@ -0,0 +1,57 @@ + + */ + protected $fillable = [ + 'tenant_id', + 'image_path', + 'caption', + 'uploaded_by', + 'display_order', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'display_order' => 'integer', + ]; + + /** + * Get the club that owns the image. + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } + + /** + * Get the user who uploaded the image. + */ + public function uploader(): BelongsTo + { + return $this->belongsTo(User::class, 'uploaded_by'); + } +} diff --git a/app/Models/ClubInstructor.php b/app/Models/ClubInstructor.php new file mode 100644 index 0000000..662c065 --- /dev/null +++ b/app/Models/ClubInstructor.php @@ -0,0 +1,71 @@ + + */ + protected $fillable = [ + 'tenant_id', + 'user_id', + 'role', + 'experience_years', + 'rating', + 'skills', + 'bio', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'experience_years' => 'integer', + 'rating' => 'decimal:2', + 'skills' => 'array', + ]; + + /** + * Get the club that owns the instructor. + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } + + /** + * Get the user associated with the instructor. + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** + * Get the packages that include this instructor. + */ + public function packages(): BelongsToMany + { + return $this->belongsToMany(ClubPackage::class, 'club_package_activities', 'instructor_id', 'package_id') + ->withTimestamps(); + } +} diff --git a/app/Models/ClubMemberSubscription.php b/app/Models/ClubMemberSubscription.php new file mode 100644 index 0000000..6b20840 --- /dev/null +++ b/app/Models/ClubMemberSubscription.php @@ -0,0 +1,112 @@ + + */ + protected $fillable = [ + 'tenant_id', + 'user_id', + 'package_id', + 'start_date', + 'end_date', + 'status', + 'payment_status', + 'amount_paid', + 'amount_due', + 'notes', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'start_date' => 'date', + 'end_date' => 'date', + 'amount_paid' => 'decimal:2', + 'amount_due' => 'decimal:2', + ]; + + /** + * Get the club that owns the subscription. + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } + + /** + * Get the member associated with the subscription. + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** + * Get the package associated with the subscription. + */ + public function package(): BelongsTo + { + return $this->belongsTo(ClubPackage::class, 'package_id'); + } + + /** + * Get the transactions for the subscription. + */ + public function transactions(): HasMany + { + return $this->hasMany(ClubTransaction::class, 'subscription_id'); + } + + /** + * Check if subscription is expiring soon (within 3 days). + */ + public function isExpiringSoon(): bool + { + if ($this->status !== 'active') { + return false; + } + + $daysUntilExpiry = Carbon::now()->diffInDays($this->end_date, false); + return $daysUntilExpiry >= 0 && $daysUntilExpiry <= 3; + } + + /** + * Check if subscription is expired. + */ + public function isExpired(): bool + { + return $this->end_date < Carbon::now(); + } + + /** + * Get days until expiry. + */ + public function daysUntilExpiry(): int + { + return Carbon::now()->diffInDays($this->end_date, false); + } +} diff --git a/app/Models/ClubMessage.php b/app/Models/ClubMessage.php new file mode 100644 index 0000000..d1a5f80 --- /dev/null +++ b/app/Models/ClubMessage.php @@ -0,0 +1,95 @@ + + */ + protected $fillable = [ + 'tenant_id', + 'sender_id', + 'recipient_id', + 'subject', + 'message', + 'is_read', + 'read_at', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'is_read' => 'boolean', + 'read_at' => 'datetime', + ]; + + /** + * Get the club that owns the message. + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } + + /** + * Get the sender of the message. + */ + public function sender(): BelongsTo + { + return $this->belongsTo(User::class, 'sender_id'); + } + + /** + * Get the recipient of the message. + */ + public function recipient(): BelongsTo + { + return $this->belongsTo(User::class, 'recipient_id'); + } + + /** + * Mark message as read. + */ + public function markAsRead(): void + { + $this->update([ + 'is_read' => true, + 'read_at' => now(), + ]); + } + + /** + * Scope a query to only include unread messages. + */ + public function scopeUnread($query) + { + return $query->where('is_read', false); + } + + /** + * Scope a query to only include read messages. + */ + public function scopeRead($query) + { + return $query->where('is_read', true); + } +} diff --git a/app/Models/ClubPackage.php b/app/Models/ClubPackage.php new file mode 100644 index 0000000..6d6fd27 --- /dev/null +++ b/app/Models/ClubPackage.php @@ -0,0 +1,90 @@ + + */ + protected $fillable = [ + 'tenant_id', + 'name', + 'cover_image', + 'type', + 'age_min', + 'age_max', + 'gender', + 'price', + 'duration_months', + 'session_count', + 'description', + 'is_active', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'age_min' => 'integer', + 'age_max' => 'integer', + 'price' => 'decimal:2', + 'duration_months' => 'integer', + 'session_count' => 'integer', + 'is_active' => 'boolean', + ]; + + /** + * Get the club that owns the package. + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } + + /** + * Get the activities included in the package. + */ + public function activities(): BelongsToMany + { + return $this->belongsToMany(ClubActivity::class, 'club_package_activities', 'package_id', 'activity_id') + ->withPivot('instructor_id') + ->withTimestamps(); + } + + /** + * Get the subscriptions for the package. + */ + public function subscriptions(): HasMany + { + return $this->hasMany(ClubMemberSubscription::class, 'package_id'); + } + + /** + * Get active subscriptions for the package. + */ + public function activeSubscriptions(): HasMany + { + return $this->hasMany(ClubMemberSubscription::class, 'package_id') + ->where('status', 'active'); + } +} diff --git a/app/Models/ClubReview.php b/app/Models/ClubReview.php new file mode 100644 index 0000000..be8b8cd --- /dev/null +++ b/app/Models/ClubReview.php @@ -0,0 +1,74 @@ + + */ + protected $fillable = [ + 'tenant_id', + 'user_id', + 'rating', + 'comment', + 'is_approved', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'rating' => 'integer', + 'is_approved' => 'boolean', + ]; + + /** + * Get the club that owns the review. + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } + + /** + * Get the user who wrote the review. + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** + * Scope a query to only include approved reviews. + */ + public function scopeApproved($query) + { + return $query->where('is_approved', true); + } + + /** + * Scope a query to only include pending reviews. + */ + public function scopePending($query) + { + return $query->where('is_approved', false); + } +} diff --git a/app/Models/ClubSocialLink.php b/app/Models/ClubSocialLink.php new file mode 100644 index 0000000..64c4bb4 --- /dev/null +++ b/app/Models/ClubSocialLink.php @@ -0,0 +1,49 @@ + + */ + protected $fillable = [ + 'tenant_id', + 'platform', + 'url', + 'icon', + 'display_order', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'display_order' => 'integer', + ]; + + /** + * Get the club that owns the social link. + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } +} diff --git a/app/Models/ClubTransaction.php b/app/Models/ClubTransaction.php new file mode 100644 index 0000000..c715e47 --- /dev/null +++ b/app/Models/ClubTransaction.php @@ -0,0 +1,95 @@ + + */ + protected $fillable = [ + 'tenant_id', + 'user_id', + 'type', + 'category', + 'amount', + 'payment_method', + 'description', + 'transaction_date', + 'reference_number', + 'subscription_id', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'amount' => 'decimal:2', + 'transaction_date' => 'date', + ]; + + /** + * Get the club that owns the transaction. + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } + + /** + * Get the user associated with the transaction. + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** + * Get the subscription associated with the transaction. + */ + public function subscription(): BelongsTo + { + return $this->belongsTo(ClubMemberSubscription::class, 'subscription_id'); + } + + /** + * Scope a query to only include income transactions. + */ + public function scopeIncome($query) + { + return $query->where('type', 'income'); + } + + /** + * Scope a query to only include expense transactions. + */ + public function scopeExpense($query) + { + return $query->where('type', 'expense'); + } + + /** + * Scope a query to only include refund transactions. + */ + public function scopeRefund($query) + { + return $query->where('type', 'refund'); + } +} diff --git a/app/Models/Permission.php b/app/Models/Permission.php new file mode 100644 index 0000000..b157da7 --- /dev/null +++ b/app/Models/Permission.php @@ -0,0 +1,32 @@ + + */ + protected $fillable = [ + 'name', + 'slug', + 'description', + ]; + + /** + * Get the roles for the permission. + */ + public function roles(): BelongsToMany + { + return $this->belongsToMany(Role::class, 'role_permission') + ->withTimestamps(); + } +} diff --git a/app/Models/Role.php b/app/Models/Role.php new file mode 100644 index 0000000..e4b8569 --- /dev/null +++ b/app/Models/Role.php @@ -0,0 +1,50 @@ + + */ + protected $fillable = [ + 'name', + 'slug', + 'description', + ]; + + /** + * Get the permissions for the role. + */ + public function permissions(): BelongsToMany + { + return $this->belongsToMany(Permission::class, 'role_permission') + ->withTimestamps(); + } + + /** + * Get the users for the role. + */ + public function users(): BelongsToMany + { + return $this->belongsToMany(User::class, 'user_roles') + ->withPivot('tenant_id') + ->withTimestamps(); + } + + /** + * Check if role has a specific permission. + */ + public function hasPermission(string $permissionSlug): bool + { + return $this->permissions()->where('slug', $permissionSlug)->exists(); + } +} diff --git a/app/Models/Tenant.php b/app/Models/Tenant.php index 404e270..a850e65 100644 --- a/app/Models/Tenant.php +++ b/app/Models/Tenant.php @@ -7,10 +7,11 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\SoftDeletes; class Tenant extends Model { - use HasFactory; + use HasFactory, SoftDeletes; /** * The attributes that are mass assignable. @@ -22,8 +23,25 @@ class Tenant extends Model 'club_name', 'slug', 'logo', + 'slogan', + 'description', + 'enrollment_fee', + 'commercial_reg_number', + 'vat_reg_number', + 'vat_percentage', + 'email', + 'phone', + 'currency', + 'timezone', + 'country', + 'address', + 'favicon', + 'cover_image', + 'owner_name', + 'owner_email', 'gps_lat', 'gps_long', + 'settings', ]; /** @@ -34,6 +52,10 @@ class Tenant extends Model protected $casts = [ 'gps_lat' => 'decimal:7', 'gps_long' => 'decimal:7', + 'enrollment_fee' => 'decimal:2', + 'vat_percentage' => 'decimal:2', + 'phone' => 'array', + 'settings' => 'array', ]; /** @@ -61,4 +83,124 @@ class Tenant extends Model { return $this->hasMany(Invoice::class); } + + /** + * Get the facilities for the club. + */ + public function facilities(): HasMany + { + return $this->hasMany(ClubFacility::class); + } + + /** + * Get the instructors for the club. + */ + public function instructors(): HasMany + { + return $this->hasMany(ClubInstructor::class); + } + + /** + * Get the activities for the club. + */ + public function activities(): HasMany + { + return $this->hasMany(ClubActivity::class); + } + + /** + * Get the packages for the club. + */ + public function packages(): HasMany + { + return $this->hasMany(ClubPackage::class); + } + + /** + * Get the subscriptions for the club. + */ + public function subscriptions(): HasMany + { + return $this->hasMany(ClubMemberSubscription::class); + } + + /** + * Get the transactions for the club. + */ + public function transactions(): HasMany + { + return $this->hasMany(ClubTransaction::class); + } + + /** + * Get the gallery images for the club. + */ + public function galleryImages(): HasMany + { + return $this->hasMany(ClubGalleryImage::class); + } + + /** + * Get the social links for the club. + */ + public function socialLinks(): HasMany + { + return $this->hasMany(ClubSocialLink::class); + } + + /** + * Get the bank accounts for the club. + */ + public function bankAccounts(): HasMany + { + return $this->hasMany(ClubBankAccount::class); + } + + /** + * Get the messages for the club. + */ + public function messages(): HasMany + { + return $this->hasMany(ClubMessage::class); + } + + /** + * Get the reviews for the club. + */ + public function reviews(): HasMany + { + return $this->hasMany(ClubReview::class); + } + + /** + * Get approved reviews for the club. + */ + public function approvedReviews(): HasMany + { + return $this->hasMany(ClubReview::class)->where('is_approved', true); + } + + /** + * Get the average rating for the club. + */ + public function getAverageRatingAttribute(): float + { + return $this->approvedReviews()->avg('rating') ?? 0; + } + + /** + * Get active members count. + */ + public function getActiveMembersCountAttribute(): int + { + return $this->subscriptions()->where('status', 'active')->distinct('user_id')->count('user_id'); + } + + /** + * Get the club URL. + */ + public function getUrlAttribute(): string + { + return url("/club/{$this->slug}"); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 4b5808c..a4d9c8f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -258,6 +258,154 @@ class User extends Authenticatable return $this->hasMany(ClubAffiliation::class, 'member_id'); } + /** + * Get the roles for the user. + */ + public function roles(): BelongsToMany + { + return $this->belongsToMany(Role::class, 'user_roles') + ->withPivot('tenant_id') + ->withTimestamps(); + } + + /** + * Get the subscriptions for the user. + */ + public function subscriptions(): HasMany + { + return $this->hasMany(ClubMemberSubscription::class); + } + + /** + * Get the transactions for the user. + */ + public function transactions(): HasMany + { + return $this->hasMany(ClubTransaction::class); + } + + /** + * Get the sent messages for the user. + */ + public function sentMessages(): HasMany + { + return $this->hasMany(ClubMessage::class, 'sender_id'); + } + + /** + * Get the received messages for the user. + */ + public function receivedMessages(): HasMany + { + return $this->hasMany(ClubMessage::class, 'recipient_id'); + } + + /** + * Get the reviews written by the user. + */ + public function reviews(): HasMany + { + return $this->hasMany(ClubReview::class); + } + + /** + * Check if user has a specific role. + */ + public function hasRole(string $roleSlug, ?int $tenantId = null): bool + { + $query = $this->roles()->where('slug', $roleSlug); + + if ($tenantId !== null) { + $query->wherePivot('tenant_id', $tenantId); + } + + return $query->exists(); + } + + /** + * Check if user has any of the given roles. + */ + public function hasAnyRole(array $roleSlugs, ?int $tenantId = null): bool + { + $query = $this->roles()->whereIn('slug', $roleSlugs); + + if ($tenantId !== null) { + $query->wherePivot('tenant_id', $tenantId); + } + + return $query->exists(); + } + + /** + * Check if user has a specific permission. + */ + public function hasPermission(string $permissionSlug, ?int $tenantId = null): bool + { + $roles = $tenantId !== null + ? $this->roles()->wherePivot('tenant_id', $tenantId)->get() + : $this->roles; + + foreach ($roles as $role) { + if ($role->hasPermission($permissionSlug)) { + return true; + } + } + + return false; + } + + /** + * Check if user is super admin. + */ + public function isSuperAdmin(): bool + { + return $this->hasRole('super-admin'); + } + + /** + * Check if user is club admin for a specific club. + */ + public function isClubAdmin(?int $tenantId = null): bool + { + return $this->hasRole('club-admin', $tenantId); + } + + /** + * Check if user is instructor for a specific club. + */ + public function isInstructor(?int $tenantId = null): bool + { + return $this->hasRole('instructor', $tenantId); + } + + /** + * Assign a role to the user. + */ + public function assignRole(string $roleSlug, ?int $tenantId = null): void + { + $role = Role::where('slug', $roleSlug)->firstOrFail(); + + $this->roles()->attach($role->id, ['tenant_id' => $tenantId]); + } + + /** + * Remove a role from the user. + */ + public function removeRole(string $roleSlug, ?int $tenantId = null): void + { + $role = Role::where('slug', $roleSlug)->first(); + + if (!$role) { + return; + } + + if ($tenantId !== null) { + $this->roles()->wherePivot('tenant_id', $tenantId)->detach($role->id); + } else { + $this->roles()->detach($role->id); + } + } + /** * Send the email verification notification. * Override to prevent sending the default Laravel notification. diff --git a/bootstrap/app.php b/bootstrap/app.php index c3928c5..5e3c8b2 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -12,7 +12,10 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - // + $middleware->alias([ + 'role' => \App\Http\Middleware\CheckRole::class, + 'permission' => \App\Http\Middleware\CheckPermission::class, + ]); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/database/migrations/2026_01_25_100000_create_roles_and_permissions_tables.php b/database/migrations/2026_01_25_100000_create_roles_and_permissions_tables.php new file mode 100644 index 0000000..ce448a6 --- /dev/null +++ b/database/migrations/2026_01_25_100000_create_roles_and_permissions_tables.php @@ -0,0 +1,64 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->text('description')->nullable(); + $table->timestamps(); + }); + + // Permissions table + Schema::create('permissions', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->text('description')->nullable(); + $table->timestamps(); + }); + + // Role-Permission pivot table + Schema::create('role_permission', function (Blueprint $table) { + $table->id(); + $table->foreignId('role_id')->constrained('roles')->cascadeOnDelete(); + $table->foreignId('permission_id')->constrained('permissions')->cascadeOnDelete(); + $table->timestamps(); + + $table->unique(['role_id', 'permission_id']); + }); + + // User-Role pivot table (with tenant_id for club-specific roles) + Schema::create('user_roles', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->foreignId('role_id')->constrained('roles')->cascadeOnDelete(); + $table->foreignId('tenant_id')->nullable()->constrained('tenants')->cascadeOnDelete(); + $table->timestamps(); + + $table->unique(['user_id', 'role_id', 'tenant_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_roles'); + Schema::dropIfExists('role_permission'); + Schema::dropIfExists('permissions'); + Schema::dropIfExists('roles'); + } +}; diff --git a/database/migrations/2026_01_25_100001_expand_tenants_table.php b/database/migrations/2026_01_25_100001_expand_tenants_table.php new file mode 100644 index 0000000..accc42e --- /dev/null +++ b/database/migrations/2026_01_25_100001_expand_tenants_table.php @@ -0,0 +1,84 @@ +string('slogan')->nullable()->after('club_name'); + $table->text('description')->nullable()->after('slogan'); + $table->decimal('enrollment_fee', 10, 2)->default(0)->after('description'); + $table->string('commercial_reg_number')->nullable()->after('enrollment_fee'); + $table->string('vat_reg_number')->nullable()->after('commercial_reg_number'); + $table->decimal('vat_percentage', 5, 2)->default(0)->after('vat_reg_number'); + + // Contact Information + $table->string('email')->nullable()->after('vat_percentage'); + $table->json('phone')->nullable()->after('email'); // {code: '+973', number: '12345678'} + $table->string('currency', 3)->default('BHD')->after('phone'); // ISO 4217 currency code + $table->string('timezone')->default('Asia/Bahrain')->after('currency'); + $table->string('country')->default('Bahrain')->after('timezone'); + $table->text('address')->nullable()->after('country'); + + // Branding Assets + $table->string('favicon')->nullable()->after('logo'); + $table->string('cover_image')->nullable()->after('favicon'); + + // Owner Information (denormalized for quick access) + $table->string('owner_name')->nullable()->after('owner_user_id'); + $table->string('owner_email')->nullable()->after('owner_name'); + + // Settings (JSON for code prefixes and other configurations) + $table->json('settings')->nullable()->after('gps_long'); + // Example settings structure: + // { + // "member_code_prefix": "MEM", + // "child_code_prefix": "CHILD", + // "invoice_code_prefix": "INV", + // "receipt_code_prefix": "REC", + // "expense_code_prefix": "EXP", + // "specialist_code_prefix": "SPEC" + // } + + // Soft Deletes + $table->softDeletes()->after('updated_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('tenants', function (Blueprint $table) { + $table->dropSoftDeletes(); + $table->dropColumn([ + 'slogan', + 'description', + 'enrollment_fee', + 'commercial_reg_number', + 'vat_reg_number', + 'vat_percentage', + 'email', + 'phone', + 'currency', + 'timezone', + 'country', + 'address', + 'favicon', + 'cover_image', + 'owner_name', + 'owner_email', + 'settings', + ]); + }); + } +}; diff --git a/database/migrations/2026_01_25_100002_create_club_facilities_table.php b/database/migrations/2026_01_25_100002_create_club_facilities_table.php new file mode 100644 index 0000000..1fa3b3e --- /dev/null +++ b/database/migrations/2026_01_25_100002_create_club_facilities_table.php @@ -0,0 +1,36 @@ +id(); + $table->foreignId('tenant_id')->constrained('tenants')->cascadeOnDelete(); + $table->string('name'); + $table->string('photo')->nullable(); + $table->text('address')->nullable(); + $table->decimal('gps_lat', 10, 7)->nullable(); + $table->decimal('gps_long', 10, 7)->nullable(); + $table->boolean('is_available')->default(true); + $table->timestamps(); + + $table->index('tenant_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('club_facilities'); + } +}; diff --git a/database/migrations/2026_01_25_100003_create_club_instructors_table.php b/database/migrations/2026_01_25_100003_create_club_instructors_table.php new file mode 100644 index 0000000..f98d7cb --- /dev/null +++ b/database/migrations/2026_01_25_100003_create_club_instructors_table.php @@ -0,0 +1,38 @@ +id(); + $table->foreignId('tenant_id')->constrained('tenants')->cascadeOnDelete(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->string('role')->default('Instructor'); // e.g., "TaeKwonDo Instructor", "Fitness Coach" + $table->integer('experience_years')->default(0); + $table->decimal('rating', 3, 2)->default(0.00); // 0.00 to 5.00 + $table->json('skills')->nullable(); // ["taekwondo", "fitness", "self-defense"] + $table->text('bio')->nullable(); + $table->timestamps(); + + $table->index('tenant_id'); + $table->index('user_id'); + $table->unique(['tenant_id', 'user_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('club_instructors'); + } +}; diff --git a/database/migrations/2026_01_25_100004_create_club_activities_table.php b/database/migrations/2026_01_25_100004_create_club_activities_table.php new file mode 100644 index 0000000..b19864c --- /dev/null +++ b/database/migrations/2026_01_25_100004_create_club_activities_table.php @@ -0,0 +1,43 @@ +id(); + $table->foreignId('tenant_id')->constrained('tenants')->cascadeOnDelete(); + $table->string('name'); // e.g., "TAEKWONDO (S.M.W) CLASS A" + $table->integer('duration_minutes')->default(60); + $table->integer('frequency_per_week')->default(3); // How many times per week + $table->foreignId('facility_id')->nullable()->constrained('club_facilities')->nullOnDelete(); + $table->json('schedule')->nullable(); + // Example schedule structure: + // [ + // {"day": "Saturday", "time": "16:00"}, + // {"day": "Monday", "time": "16:00"}, + // {"day": "Wednesday", "time": "16:00"} + // ] + $table->text('description')->nullable(); + $table->timestamps(); + + $table->index('tenant_id'); + $table->index('facility_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('club_activities'); + } +}; diff --git a/database/migrations/2026_01_25_100005_create_club_packages_table.php b/database/migrations/2026_01_25_100005_create_club_packages_table.php new file mode 100644 index 0000000..22f5d06 --- /dev/null +++ b/database/migrations/2026_01_25_100005_create_club_packages_table.php @@ -0,0 +1,41 @@ +id(); + $table->foreignId('tenant_id')->constrained('tenants')->cascadeOnDelete(); + $table->string('name'); // e.g., "Juniors & Fighters (S.M.W) Group A" + $table->string('cover_image')->nullable(); + $table->enum('type', ['single', 'multi'])->default('single'); // Single Activity or Multi Activity + $table->integer('age_min')->nullable(); // Minimum age + $table->integer('age_max')->nullable(); // Maximum age + $table->enum('gender', ['mixed', 'male', 'female'])->default('mixed'); + $table->decimal('price', 10, 2); // Price in club's currency + $table->integer('duration_months')->default(1); // Package duration in months + $table->integer('session_count')->default(0); // Total sessions included (0 = unlimited) + $table->text('description')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + + $table->index('tenant_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('club_packages'); + } +}; diff --git a/database/migrations/2026_01_25_100006_create_club_package_activities_table.php b/database/migrations/2026_01_25_100006_create_club_package_activities_table.php new file mode 100644 index 0000000..c72de53 --- /dev/null +++ b/database/migrations/2026_01_25_100006_create_club_package_activities_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('package_id')->constrained('club_packages')->cascadeOnDelete(); + $table->foreignId('activity_id')->constrained('club_activities')->cascadeOnDelete(); + $table->foreignId('instructor_id')->nullable()->constrained('club_instructors')->nullOnDelete(); + $table->timestamps(); + + $table->unique(['package_id', 'activity_id']); + $table->index('package_id'); + $table->index('activity_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('club_package_activities'); + } +}; diff --git a/database/migrations/2026_01_25_100007_create_club_member_subscriptions_table.php b/database/migrations/2026_01_25_100007_create_club_member_subscriptions_table.php new file mode 100644 index 0000000..75dc3fe --- /dev/null +++ b/database/migrations/2026_01_25_100007_create_club_member_subscriptions_table.php @@ -0,0 +1,43 @@ +id(); + $table->foreignId('tenant_id')->constrained('tenants')->cascadeOnDelete(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->foreignId('package_id')->constrained('club_packages')->cascadeOnDelete(); + $table->date('start_date'); + $table->date('end_date'); + $table->enum('status', ['active', 'expired', 'cancelled', 'pending'])->default('pending'); + $table->enum('payment_status', ['paid', 'unpaid', 'partial', 'refunded'])->default('unpaid'); + $table->decimal('amount_paid', 10, 2)->default(0); + $table->decimal('amount_due', 10, 2)->default(0); + $table->text('notes')->nullable(); + $table->timestamps(); + + $table->index('tenant_id'); + $table->index('user_id'); + $table->index('package_id'); + $table->index('status'); + $table->index('end_date'); // For expiring subscriptions queries + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('club_member_subscriptions'); + } +}; diff --git a/database/migrations/2026_01_25_100008_create_club_transactions_table.php b/database/migrations/2026_01_25_100008_create_club_transactions_table.php new file mode 100644 index 0000000..bbbb714 --- /dev/null +++ b/database/migrations/2026_01_25_100008_create_club_transactions_table.php @@ -0,0 +1,43 @@ +id(); + $table->foreignId('tenant_id')->constrained('tenants')->cascadeOnDelete(); + $table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete(); // Member associated with transaction + $table->enum('type', ['income', 'expense', 'refund'])->default('income'); + $table->string('category')->nullable(); // e.g., "Membership Fee", "Equipment", "Utilities" + $table->decimal('amount', 10, 2); + $table->enum('payment_method', ['cash', 'card', 'bank_transfer', 'online', 'other'])->default('cash'); + $table->text('description')->nullable(); + $table->date('transaction_date'); + $table->string('reference_number')->nullable(); // Invoice/Receipt number + $table->foreignId('subscription_id')->nullable()->constrained('club_member_subscriptions')->nullOnDelete(); + $table->timestamps(); + + $table->index('tenant_id'); + $table->index('user_id'); + $table->index('type'); + $table->index('transaction_date'); + $table->index('reference_number'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('club_transactions'); + } +}; diff --git a/database/migrations/2026_01_25_100009_create_club_gallery_images_table.php b/database/migrations/2026_01_25_100009_create_club_gallery_images_table.php new file mode 100644 index 0000000..fd42676 --- /dev/null +++ b/database/migrations/2026_01_25_100009_create_club_gallery_images_table.php @@ -0,0 +1,35 @@ +id(); + $table->foreignId('tenant_id')->constrained('tenants')->cascadeOnDelete(); + $table->string('image_path'); + $table->string('caption')->nullable(); + $table->foreignId('uploaded_by')->constrained('users')->cascadeOnDelete(); + $table->integer('display_order')->default(0); + $table->timestamps(); + + $table->index('tenant_id'); + $table->index('uploaded_by'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('club_gallery_images'); + } +}; diff --git a/database/migrations/2026_01_25_100010_create_club_social_links_table.php b/database/migrations/2026_01_25_100010_create_club_social_links_table.php new file mode 100644 index 0000000..746ea61 --- /dev/null +++ b/database/migrations/2026_01_25_100010_create_club_social_links_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('tenant_id')->constrained('tenants')->cascadeOnDelete(); + $table->string('platform'); // e.g., "Instagram", "TikTok", "Snapchat", "WhatsApp", "Phone", "Email" + $table->string('url'); // Full URL or contact info + $table->string('icon')->nullable(); // Icon class or path + $table->integer('display_order')->default(0); + $table->timestamps(); + + $table->index('tenant_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('club_social_links'); + } +}; diff --git a/database/migrations/2026_01_25_100011_create_club_bank_accounts_table.php b/database/migrations/2026_01_25_100011_create_club_bank_accounts_table.php new file mode 100644 index 0000000..d685ab4 --- /dev/null +++ b/database/migrations/2026_01_25_100011_create_club_bank_accounts_table.php @@ -0,0 +1,36 @@ +id(); + $table->foreignId('tenant_id')->constrained('tenants')->cascadeOnDelete(); + $table->string('bank_name'); + $table->string('account_name'); + $table->text('account_number'); // Encrypted + $table->text('iban')->nullable(); // Encrypted + $table->text('swift_code')->nullable(); // Encrypted + $table->boolean('is_primary')->default(false); + $table->timestamps(); + + $table->index('tenant_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('club_bank_accounts'); + } +}; diff --git a/database/migrations/2026_01_25_100012_create_club_messages_table.php b/database/migrations/2026_01_25_100012_create_club_messages_table.php new file mode 100644 index 0000000..c129e91 --- /dev/null +++ b/database/migrations/2026_01_25_100012_create_club_messages_table.php @@ -0,0 +1,39 @@ +id(); + $table->foreignId('tenant_id')->constrained('tenants')->cascadeOnDelete(); + $table->foreignId('sender_id')->constrained('users')->cascadeOnDelete(); + $table->foreignId('recipient_id')->constrained('users')->cascadeOnDelete(); + $table->string('subject')->nullable(); + $table->text('message'); + $table->boolean('is_read')->default(false); + $table->timestamp('read_at')->nullable(); + $table->timestamps(); + + $table->index('tenant_id'); + $table->index('sender_id'); + $table->index('recipient_id'); + $table->index('is_read'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('club_messages'); + } +}; diff --git a/database/migrations/2026_01_25_100013_create_club_reviews_table.php b/database/migrations/2026_01_25_100013_create_club_reviews_table.php new file mode 100644 index 0000000..e7fd333 --- /dev/null +++ b/database/migrations/2026_01_25_100013_create_club_reviews_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('tenant_id')->constrained('tenants')->cascadeOnDelete(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->integer('rating'); // 1-5 stars + $table->text('comment')->nullable(); + $table->boolean('is_approved')->default(false); + $table->timestamps(); + + $table->index('tenant_id'); + $table->index('user_id'); + $table->index('rating'); + $table->unique(['tenant_id', 'user_id']); // One review per user per club + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('club_reviews'); + } +}; diff --git a/database/seeders/RolePermissionSeeder.php b/database/seeders/RolePermissionSeeder.php new file mode 100644 index 0000000..eba2fb0 --- /dev/null +++ b/database/seeders/RolePermissionSeeder.php @@ -0,0 +1,123 @@ + 'Manage All Clubs', 'slug' => 'manage-all-clubs', 'description' => 'Create, edit, delete any club'], + ['name' => 'Manage All Members', 'slug' => 'manage-all-members', 'description' => 'View and manage all platform members'], + ['name' => 'Database Backup', 'slug' => 'database-backup', 'description' => 'Backup and restore database'], + ['name' => 'View Platform Analytics', 'slug' => 'view-platform-analytics', 'description' => 'View platform-wide analytics'], + + // Club Admin Permissions + ['name' => 'Manage Club Details', 'slug' => 'manage-club-details', 'description' => 'Edit club information and settings'], + ['name' => 'Manage Facilities', 'slug' => 'manage-facilities', 'description' => 'Create, edit, delete facilities'], + ['name' => 'Manage Instructors', 'slug' => 'manage-instructors', 'description' => 'Add, edit, remove instructors'], + ['name' => 'Manage Activities', 'slug' => 'manage-activities', 'description' => 'Create, edit, delete activities'], + ['name' => 'Manage Packages', 'slug' => 'manage-packages', 'description' => 'Create, edit, delete packages'], + ['name' => 'Manage Members', 'slug' => 'manage-members', 'description' => 'Add, edit, remove club members'], + ['name' => 'Manage Financials', 'slug' => 'manage-financials', 'description' => 'View and manage club finances'], + ['name' => 'Manage Gallery', 'slug' => 'manage-gallery', 'description' => 'Upload and manage gallery images'], + ['name' => 'Manage Messages', 'slug' => 'manage-messages', 'description' => 'Send and receive messages'], + ['name' => 'View Analytics', 'slug' => 'view-analytics', 'description' => 'View club analytics and reports'], + + // Instructor Permissions + ['name' => 'View Members', 'slug' => 'view-members', 'description' => 'View club members'], + ['name' => 'Mark Attendance', 'slug' => 'mark-attendance', 'description' => 'Mark member attendance'], + ['name' => 'Send Messages', 'slug' => 'send-messages', 'description' => 'Send messages to members'], + + // Member Permissions + ['name' => 'View Own Profile', 'slug' => 'view-own-profile', 'description' => 'View own profile and subscriptions'], + ['name' => 'Update Own Profile', 'slug' => 'update-own-profile', 'description' => 'Update own profile information'], + ['name' => 'View Club Info', 'slug' => 'view-club-info', 'description' => 'View club information'], + ]; + + foreach ($permissions as $permission) { + Permission::firstOrCreate( + ['slug' => $permission['slug']], + $permission + ); + } + + // Create Roles + $roles = [ + [ + 'name' => 'Super Admin', + 'slug' => 'super-admin', + 'description' => 'Platform administrator with full access', + 'permissions' => [ + 'manage-all-clubs', + 'manage-all-members', + 'database-backup', + 'view-platform-analytics', + ] + ], + [ + 'name' => 'Club Admin', + 'slug' => 'club-admin', + 'description' => 'Club owner/administrator with full club access', + 'permissions' => [ + 'manage-club-details', + 'manage-facilities', + 'manage-instructors', + 'manage-activities', + 'manage-packages', + 'manage-members', + 'manage-financials', + 'manage-gallery', + 'manage-messages', + 'view-analytics', + ] + ], + [ + 'name' => 'Instructor', + 'slug' => 'instructor', + 'description' => 'Club instructor with limited access', + 'permissions' => [ + 'view-members', + 'mark-attendance', + 'send-messages', + 'view-club-info', + ] + ], + [ + 'name' => 'Member', + 'slug' => 'member', + 'description' => 'Club member with basic access', + 'permissions' => [ + 'view-own-profile', + 'update-own-profile', + 'view-club-info', + ] + ], + ]; + + foreach ($roles as $roleData) { + $role = Role::firstOrCreate( + ['slug' => $roleData['slug']], + [ + 'name' => $roleData['name'], + 'description' => $roleData['description'], + ] + ); + + // Attach permissions to role + $permissionIds = Permission::whereIn('slug', $roleData['permissions'])->pluck('id'); + $role->permissions()->sync($permissionIds); + } + + $this->command->info('Roles and permissions seeded successfully!'); + } +} diff --git a/resources/views/admin/platform/backup.blade.php b/resources/views/admin/platform/backup.blade.php new file mode 100644 index 0000000..7a57123 --- /dev/null +++ b/resources/views/admin/platform/backup.blade.php @@ -0,0 +1,195 @@ +@extends('layouts.admin') + +@section('admin-content') +
+ +
+

Database Backup & Restore

+

Manage platform database backups

+
+ + + + + +
+ +
+
+
+
+ +
+
Download Backup
+

+ Export the complete database as a JSON file. This includes all tables from the public schema. +

+ + Download Full Backup + + + File format: JSON + +
+
+
+ + +
+
+
+
+ +
+
Restore Database
+

+ Upload a JSON backup file to restore the database. This will overwrite all existing data! +

+ + + Use with extreme caution + +
+
+
+ + +
+
+
+
+ +
+
Export Auth Users
+

+ Download all authentication users with encrypted passwords for migration purposes. +

+ + Export Users + + + Includes encrypted passwords + +
+
+
+
+ + +
+
+
Best Practices
+
+
+
+
+
Backup Guidelines
+
    +
  • + + Schedule regular automated backups (daily recommended) +
  • +
  • + + Store backups in multiple secure locations +
  • +
  • + + Test backup restoration in a staging environment +
  • +
  • + + Keep backups for at least 30 days +
  • +
  • + + Document your backup and restore procedures +
  • +
+
+
+
Restore Warnings
+
    +
  • + + Always backup current data before restoring +
  • +
  • + + Verify backup file integrity before restoration +
  • +
  • + + Test restore in staging environment first +
  • +
  • + + Notify all users before performing restore +
  • +
  • + + Restoration will overwrite ALL existing data +
  • +
+
+
+
+
+
+ + + + +@push('scripts') + +@endpush +@endsection diff --git a/resources/views/admin/platform/clubs.blade.php b/resources/views/admin/platform/clubs.blade.php new file mode 100644 index 0000000..7fac62f --- /dev/null +++ b/resources/views/admin/platform/clubs.blade.php @@ -0,0 +1,198 @@ +@extends('layouts.admin') + +@section('admin-content') +
+ +
+

All Clubs

+

Manage all clubs on the platform

+
+ + + + + + @if($clubs->count() > 0) + + + +
+ {{ $clubs->links() }} +
+ @else +
+
+ +
No Clubs Found
+

+ @if($search) + No clubs match your search criteria. + @else + Get started by creating your first club. + @endif +

+ @if(!$search) + + Add New Club + + @endif +
+
+ @endif +
+ +@push('styles') + +@endpush + +@push('scripts') + +@endpush +@endsection diff --git a/resources/views/admin/platform/create-club.blade.php b/resources/views/admin/platform/create-club.blade.php new file mode 100644 index 0000000..577c492 --- /dev/null +++ b/resources/views/admin/platform/create-club.blade.php @@ -0,0 +1,187 @@ +@extends('layouts.admin') + +@section('page-title', 'Create New Club') +@section('page-subtitle', 'Add a new club to the platform') + +@section('content') +
+
+
+
+
Club Information
+
+
+
+ @csrf + + +
+ + + @error('owner_user_id') +
{{ $message }}
+ @enderror + The user who will manage this club +
+ + +
Basic Information
+ +
+
+ + + @error('club_name') +
{{ $message }}
+ @enderror +
+
+ + + @error('slug') +
{{ $message }}
+ @enderror + URL-friendly identifier (e.g., bh-taekwondo) +
+
+ + +
Contact Information
+ +
+
+ + + @error('email') +
{{ $message }}
+ @enderror +
+
+ +
+ + +
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
Location
+ +
+ + + @error('address') +
{{ $message }}
+ @enderror +
+ +
+
+ + + @error('gps_lat') +
{{ $message }}
+ @enderror +
+
+ + + @error('gps_long') +
{{ $message }}
+ @enderror +
+
+ + +
Branding
+ +
+
+ + + @error('logo') +
{{ $message }}
+ @enderror + Recommended: Square image, max 2MB +
+
+ + + @error('cover_image') +
{{ $message }}
+ @enderror + Recommended: 1200x400px, max 2MB +
+
+ + +
+ + Cancel + + +
+
+
+
+
+
+@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/admin/platform/edit-club.blade.php b/resources/views/admin/platform/edit-club.blade.php new file mode 100644 index 0000000..a670078 --- /dev/null +++ b/resources/views/admin/platform/edit-club.blade.php @@ -0,0 +1,187 @@ +@extends('layouts.admin') + +@section('page-title', 'Edit Club') +@section('page-subtitle', 'Update club information') + +@section('content') +
+
+
+
+
{{ $club->club_name }}
+ {{ $club->slug }} +
+
+
+ @csrf + @method('PUT') + + +
+ + + @error('owner_user_id') +
{{ $message }}
+ @enderror +
+ + +
Basic Information
+ +
+
+ + + @error('club_name') +
{{ $message }}
+ @enderror +
+
+ + + @error('slug') +
{{ $message }}
+ @enderror + URL-friendly identifier +
+
+ + +
Contact Information
+ +
+
+ + + @error('email') +
{{ $message }}
+ @enderror +
+
+ +
+ + +
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
Location
+ +
+ + + @error('address') +
{{ $message }}
+ @enderror +
+ +
+
+ + + @error('gps_lat') +
{{ $message }}
+ @enderror +
+
+ + + @error('gps_long') +
{{ $message }}
+ @enderror +
+
+ + +
Branding
+ +
+
+ + @if($club->logo) +
+ Current Logo + Current logo +
+ @endif + + @error('logo') +
{{ $message }}
+ @enderror + Leave empty to keep current logo +
+
+ + @if($club->cover_image) +
+ Current Cover + Current cover image +
+ @endif + + @error('cover_image') +
{{ $message }}
+ @enderror + Leave empty to keep current cover +
+
+ + +
+ + Cancel + + +
+
+
+
+
+
+@endsection diff --git a/resources/views/admin/platform/index.blade.php b/resources/views/admin/platform/index.blade.php new file mode 100644 index 0000000..7b195ad --- /dev/null +++ b/resources/views/admin/platform/index.blade.php @@ -0,0 +1,185 @@ +@extends('layouts.admin') + +@section('admin-content') +
+ +
+
+

All Clubs

+

Manage all clubs on the platform

+
+ +
+ + +
+ + +
+ + + @if($clubs->isEmpty()) +
+

No clubs found. Create your first club to get started.

+
+ @else +
+ @foreach($clubs as $club) +
+
+ +
+ @if($club->cover_image) + {{ $club->club_name }} + @else +
+ +
+ @endif + + + @if($club->logo) +
+
+ {{ $club->club_name }} logo +
+
+ @endif + + +
+ + Admin + +
+ + + @if($club->rating) +
+ + + {{ number_format($club->rating, 1) }} + +
+ @endif +
+ + +
+
+

{{ $club->club_name }}

+ @if($club->location) +
+ + {{ $club->location }} +
+ @endif +
+ + +
+
+
+ +

{{ $club->members_count ?? 0 }}

+

Members

+
+
+
+
+ +

{{ $club->packages_count ?? 0 }}

+

Packages

+
+
+
+
+ +

{{ $club->trainers_count ?? 0 }}

+

Trainers

+
+
+
+
+
+
+ @endforeach +
+ @endif +
+ +@push('styles') + +@endpush + +@push('scripts') + +@endpush +@endsection diff --git a/resources/views/admin/platform/members.blade.php b/resources/views/admin/platform/members.blade.php new file mode 100644 index 0000000..59060fa --- /dev/null +++ b/resources/views/admin/platform/members.blade.php @@ -0,0 +1,281 @@ +@extends('layouts.admin') + +@section('admin-content') +
+ +
+

All Members

+

Manage all platform members

+
+ + +
+
+ +
+
+ + +
+
+ + + @if($members->count() > 0) +
+ @foreach($members as $member) +
+ +
+ +
+
+
+
+ @if($member->profile_picture) + {{ $member->full_name }} + @else +
+ {{ strtoupper(substr($member->full_name, 0, 1)) }} +
+ @endif +
+
+
+
{{ $member->full_name }}
+
+ @php + $age = $member->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 + {{ $ageGroup }} + @if($member->member_clubs_count > 0) + {{ $member->member_clubs_count }} {{ Str::plural('Club', $member->member_clubs_count) }} + @endif +
+
+
+
+ + +
+ @if($member->mobile && isset($member->mobile['number'])) +
+ + {{ $member->mobile['code'] ?? '' }} {{ $member->mobile['number'] }} +
+ @endif + @if($member->email) +
+ + {{ $member->email }} +
+ @endif +
+ + +
+
+
+
Gender
+
{{ $member->gender == 'm' ? 'Male' : 'Female' }}
+
+
+
Age
+
{{ $member->age }} years
+
+
+
+
+
Nationality
+
{{ $member->nationality }}
+
+
+
Horoscope
+
+ @php + $horoscopeSymbols = [ + 'Aries' => '♈', + 'Taurus' => '♉', + 'Gemini' => '♊', + 'Cancer' => '♋', + 'Leo' => '♌', + 'Virgo' => '♍', + 'Libra' => '♎', + 'Scorpio' => '♏', + 'Sagittarius' => '♐', + 'Capricorn' => '♑', + 'Aquarius' => '♒', + 'Pisces' => '♓' + ]; + $horoscope = $member->horoscope ?? 'N/A'; + $symbol = $horoscopeSymbols[$horoscope] ?? ''; + @endphp + {{ $symbol }} {{ $horoscope }} +
+
+
+
+
+ Next Birthday + + @if($member->birthdate) + {{ $member->birthdate->copy()->year(now()->year)->isFuture() + ? $member->birthdate->copy()->year(now()->year)->diffForHumans(['parts' => 2, 'syntax' => \Carbon\CarbonInterface::DIFF_ABSOLUTE]) + : $member->birthdate->copy()->year(now()->year + 1)->diffForHumans(['parts' => 2, 'syntax' => \Carbon\CarbonInterface::DIFF_ABSOLUTE]) }} + @else + N/A + @endif + +
+
+ Member Since + {{ $member->created_at->format('d/m/Y') }} +
+
+
+ + +
+
+ + PLATFORM MEMBER + +
+
+
+
+
+ @endforeach +
+ + +
+ {{ $members->links() }} +
+ @else +
+
+ +
No Members Found
+

+ @if($search) + No members match your search criteria. + @else + No members registered on the platform yet. + @endif +

+
+
+ @endif +
+ +@push('styles') + +@endpush + +@push('scripts') + +@endpush +@endsection diff --git a/resources/views/clubs/explore.blade.php b/resources/views/clubs/explore.blade.php index 901cf17..fb678fc 100644 --- a/resources/views/clubs/explore.blade.php +++ b/resources/views/clubs/explore.blade.php @@ -84,12 +84,16 @@ @@ -155,56 +158,20 @@ } .club-card { - transition: all 0.3s ease; - border: none; - border-radius: 12px; - overflow: hidden; + transition: all 0.3s ease-in-out; } .club-card:hover { - transform: translateY(-5px); - box-shadow: 0 8px 20px rgba(0,0,0,0.15); + transform: translateY(-8px); + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04) !important; } - .club-card-img { - height: 200px; - object-fit: cover; - width: 100%; + .club-card:hover .club-cover-img { + transform: scale(1.1); } - .club-badge { - position: absolute; - top: 10px; - right: 10px; - background: #0d6efd; - color: white; - padding: 0.25rem 0.75rem; - border-radius: 20px; - font-size: 0.75rem; - font-weight: 600; - } - - .stat-box { - background: hsl(var(--muted)); - border-radius: 8px; - padding: 0.75rem; - text-align: center; - } - - .stat-box i { - font-size: 1.25rem; - color: hsl(var(--primary)); - } - - .stat-box .stat-number { - font-size: 1.25rem; - font-weight: 700; - color: hsl(var(--foreground)); - } - - .stat-box .stat-label { - font-size: 0.75rem; - color: hsl(var(--muted-foreground)); + .club-card:hover .club-title { + color: #dc3545 !important; } /* Pulsing animation for location marker */ @@ -259,6 +226,7 @@ + + diff --git a/routes/web.php b/routes/web.php index 952ec29..4bc87c2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -80,6 +80,28 @@ Route::middleware(['auth'])->group(function () { Route::get('/clubs/all', [ClubController::class, 'all'])->name('clubs.all'); }); +// Platform Admin routes (Super Admin only) +Route::middleware(['auth', 'verified', 'role:super-admin'])->prefix('admin')->name('admin.')->group(function () { + Route::get('/', [App\Http\Controllers\Admin\PlatformController::class, 'index'])->name('platform.index'); + + // All Clubs Management + Route::get('/clubs', [App\Http\Controllers\Admin\PlatformController::class, 'clubs'])->name('platform.clubs'); + Route::get('/clubs/create', [App\Http\Controllers\Admin\PlatformController::class, 'createClub'])->name('platform.clubs.create'); + Route::post('/clubs', [App\Http\Controllers\Admin\PlatformController::class, 'storeClub'])->name('platform.clubs.store'); + Route::get('/clubs/{club}/edit', [App\Http\Controllers\Admin\PlatformController::class, 'editClub'])->name('platform.clubs.edit'); + Route::put('/clubs/{club}', [App\Http\Controllers\Admin\PlatformController::class, 'updateClub'])->name('platform.clubs.update'); + Route::delete('/clubs/{club}', [App\Http\Controllers\Admin\PlatformController::class, 'destroyClub'])->name('platform.clubs.destroy'); + + // All Members Management + Route::get('/members', [App\Http\Controllers\Admin\PlatformController::class, 'members'])->name('platform.members'); + + // Database Backup & Restore + Route::get('/backup', [App\Http\Controllers\Admin\PlatformController::class, 'backup'])->name('platform.backup'); + Route::get('/backup/download', [App\Http\Controllers\Admin\PlatformController::class, 'downloadBackup'])->name('platform.backup.download'); + Route::post('/backup/restore', [App\Http\Controllers\Admin\PlatformController::class, 'restoreBackup'])->name('platform.backup.restore'); + Route::get('/backup/export-users', [App\Http\Controllers\Admin\PlatformController::class, 'exportAuthUsers'])->name('platform.backup.export-users'); +}); + // Family routes Route::middleware(['auth', 'verified'])->group(function () { Route::get('/profile', [FamilyController::class, 'profile'])->name('profile.show');