diff --git a/app/Http/Controllers/FamilyController.php b/app/Http/Controllers/FamilyController.php new file mode 100644 index 0000000..2e5e576 --- /dev/null +++ b/app/Http/Controllers/FamilyController.php @@ -0,0 +1,168 @@ +familyService = $familyService; + } + + /** + * Display the family dashboard. + * + * @return \Illuminate\View\View + */ + public function dashboard() + { + $user = Auth::user(); + $dependents = UserRelationship::where('guardian_user_id', $user->id) + ->with('dependent') + ->get(); + $familyInvoices = $this->familyService->getFamilyInvoices($user->id); + + return view('family.dashboard', compact('user', 'dependents', 'familyInvoices')); + } + + /** + * Show the form for creating a new family member. + * + * @return \Illuminate\View\View + */ + public function create() + { + return view('family.create'); + } + + /** + * Store a newly created family member in storage. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function store(Request $request) + { + $validated = $request->validate([ + 'full_name' => 'required|string|max:255', + 'email' => 'nullable|email|max:255', + 'gender' => 'required|in:male,female', + 'birthdate' => 'required|date', + 'blood_type' => 'nullable|string|max:10', + 'nationality' => 'required|string|max:100', + 'relationship_type' => 'required|string|max:50', + 'is_billing_contact' => 'boolean', + ]); + + $guardian = Auth::user(); + $dependent = $this->familyService->createDependent($guardian, $validated); + + return redirect()->route('family.dashboard') + ->with('success', 'Family member added successfully.'); + } + + /** + * Display the specified family member. + * + * @param int $id + * @return \Illuminate\View\View + */ + public function show($id) + { + $user = Auth::user(); + $relationship = UserRelationship::where('guardian_user_id', $user->id) + ->where('dependent_user_id', $id) + ->with('dependent') + ->firstOrFail(); + + return view('family.show', compact('relationship')); + } + + /** + * Show the form for editing the specified family member. + * + * @param int $id + * @return \Illuminate\View\View + */ + public function edit($id) + { + $user = Auth::user(); + $relationship = UserRelationship::where('guardian_user_id', $user->id) + ->where('dependent_user_id', $id) + ->with('dependent') + ->firstOrFail(); + + return view('family.edit', compact('relationship')); + } + + /** + * Update the specified family member in storage. + * + * @param \Illuminate\Http\Request $request + * @param int $id + * @return \Illuminate\Http\RedirectResponse + */ + public function update(Request $request, $id) + { + $validated = $request->validate([ + 'full_name' => 'required|string|max:255', + 'email' => 'nullable|email|max:255', + 'gender' => 'required|in:male,female', + 'birthdate' => 'required|date', + 'blood_type' => 'nullable|string|max:10', + 'nationality' => 'required|string|max:100', + 'relationship_type' => 'required|string|max:50', + 'is_billing_contact' => 'boolean', + ]); + + $user = Auth::user(); + $relationship = UserRelationship::where('guardian_user_id', $user->id) + ->where('dependent_user_id', $id) + ->firstOrFail(); + + $dependent = User::findOrFail($id); + $dependent->update([ + 'full_name' => $validated['full_name'], + 'email' => $validated['email'], + 'gender' => $validated['gender'], + 'birthdate' => $validated['birthdate'], + 'blood_type' => $validated['blood_type'], + 'nationality' => $validated['nationality'], + ]); + + $relationship->update([ + 'relationship_type' => $validated['relationship_type'], + 'is_billing_contact' => $validated['is_billing_contact'] ?? false, + ]); + + return redirect()->route('family.dashboard') + ->with('success', 'Family member updated successfully.'); + } + + /** + * Remove the specified family member from storage. + * + * @param int $id + * @return \Illuminate\Http\RedirectResponse + */ + public function destroy($id) + { + $user = Auth::user(); + $relationship = UserRelationship::where('guardian_user_id', $user->id) + ->where('dependent_user_id', $id) + ->firstOrFail(); + + $relationship->delete(); + + return redirect()->route('family.dashboard') + ->with('success', 'Family member removed successfully.'); + } +} diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php new file mode 100644 index 0000000..fb3ddbf --- /dev/null +++ b/app/Http/Controllers/InvoiceController.php @@ -0,0 +1,92 @@ +id) + ->with(['student', 'tenant']) + ->get(); + + return view('invoices.index', compact('invoices')); + } + + /** + * Display the specified invoice. + * + * @param int $id + * @return \Illuminate\View\View + */ + public function show($id) + { + $user = Auth::user(); + $invoice = Invoice::where('id', $id) + ->where('payer_user_id', $user->id) + ->with(['student', 'tenant']) + ->firstOrFail(); + + return view('invoices.show', compact('invoice')); + } + + /** + * Process payment for the specified invoice. + * + * @param int $id + * @return \Illuminate\Http\RedirectResponse + */ + public function pay($id) + { + $user = Auth::user(); + $invoice = Invoice::where('id', $id) + ->where('payer_user_id', $user->id) + ->firstOrFail(); + + // In a real application, this would integrate with a payment gateway + $invoice->update([ + 'status' => 'paid' + ]); + + return redirect()->route('invoices.show', $invoice->id) + ->with('success', 'Payment processed successfully.'); + } + + /** + * Process payment for all unpaid invoices. + * + * @return \Illuminate\Http\RedirectResponse + */ + public function payAll() + { + $user = Auth::user(); + $invoices = Invoice::where('payer_user_id', $user->id) + ->where('status', '!=', 'paid') + ->get(); + + // In a real application, this would integrate with a payment gateway + foreach ($invoices as $invoice) { + $invoice->update([ + 'status' => 'paid' + ]); + } + + return redirect()->route('invoices.index') + ->with('success', 'All payments processed successfully.'); + } +} diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php new file mode 100644 index 0000000..608e1a7 --- /dev/null +++ b/app/Models/Invoice.php @@ -0,0 +1,60 @@ + + */ + protected $fillable = [ + 'tenant_id', + 'amount', + 'status', + 'due_date', + 'student_user_id', + 'payer_user_id', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'amount' => 'decimal:2', + 'due_date' => 'date', + ]; + + /** + * Get the tenant that owns the invoice. + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } + + /** + * Get the student user that owns the invoice. + */ + public function student(): BelongsTo + { + return $this->belongsTo(User::class, 'student_user_id'); + } + + /** + * Get the payer user that owns the invoice. + */ + public function payer(): BelongsTo + { + return $this->belongsTo(User::class, 'payer_user_id'); + } +} diff --git a/app/Models/Membership.php b/app/Models/Membership.php new file mode 100644 index 0000000..01eec1d --- /dev/null +++ b/app/Models/Membership.php @@ -0,0 +1,48 @@ + + */ + protected $fillable = [ + 'tenant_id', + 'user_id', + 'status', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'status' => 'string', + ]; + + /** + * Get the tenant that owns the membership. + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } + + /** + * Get the user that owns the membership. + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/Tenant.php b/app/Models/Tenant.php new file mode 100644 index 0000000..404e270 --- /dev/null +++ b/app/Models/Tenant.php @@ -0,0 +1,64 @@ + + */ + protected $fillable = [ + 'owner_user_id', + 'club_name', + 'slug', + 'logo', + 'gps_lat', + 'gps_long', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'gps_lat' => 'decimal:7', + 'gps_long' => 'decimal:7', + ]; + + /** + * Get the owner user that owns the tenant. + */ + public function owner(): BelongsTo + { + return $this->belongsTo(User::class, 'owner_user_id'); + } + + /** + * Get the members for the tenant. + */ + public function members(): BelongsToMany + { + return $this->belongsToMany(User::class, 'memberships') + ->withPivot('status') + ->withTimestamps(); + } + + /** + * Get the invoices for the tenant. + */ + public function invoices(): HasMany + { + return $this->hasMany(Invoice::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 749c7b7..54233e7 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -6,6 +6,10 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Casts\Attribute; +use Carbon\Carbon; class User extends Authenticatable { @@ -18,9 +22,17 @@ class User extends Authenticatable * @var list */ protected $fillable = [ - 'name', + 'full_name', 'email', + 'mobile', 'password', + 'gender', + 'birthdate', + 'blood_type', + 'nationality', + 'addresses', + 'social_links', + 'media_gallery', ]; /** @@ -43,6 +55,146 @@ class User extends Authenticatable return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', + 'birthdate' => 'date', + 'addresses' => 'array', + 'social_links' => 'array', + 'media_gallery' => 'array', ]; } + + /** + * Get the user's age based on birthdate. + */ + protected function age(): Attribute + { + return Attribute::make( + get: function () { + if (!$this->birthdate) { + return null; + } + return Carbon::parse($this->birthdate)->age; + } + ); + } + + /** + * Get the user's horoscope based on birthdate. + */ + protected function horoscope(): Attribute + { + return Attribute::make( + get: function () { + if (!$this->birthdate) { + return null; + } + + $month = $this->birthdate->month; + $day = $this->birthdate->day; + + if (($month == 3 && $day >= 21) || ($month == 4 && $day <= 19)) { + return 'Aries'; + } elseif (($month == 4 && $day >= 20) || ($month == 5 && $day <= 20)) { + return 'Taurus'; + } elseif (($month == 5 && $day >= 21) || ($month == 6 && $day <= 20)) { + return 'Gemini'; + } elseif (($month == 6 && $day >= 21) || ($month == 7 && $day <= 22)) { + return 'Cancer'; + } elseif (($month == 7 && $day >= 23) || ($month == 8 && $day <= 22)) { + return 'Leo'; + } elseif (($month == 8 && $day >= 23) || ($month == 9 && $day <= 22)) { + return 'Virgo'; + } elseif (($month == 9 && $day >= 23) || ($month == 10 && $day <= 22)) { + return 'Libra'; + } elseif (($month == 10 && $day >= 23) || ($month == 11 && $day <= 21)) { + return 'Scorpio'; + } elseif (($month == 11 && $day >= 22) || ($month == 12 && $day <= 21)) { + return 'Sagittarius'; + } elseif (($month == 12 && $day >= 22) || ($month == 1 && $day <= 19)) { + return 'Capricorn'; + } elseif (($month == 1 && $day >= 20) || ($month == 2 && $day <= 18)) { + return 'Aquarius'; + } else { + return 'Pisces'; + } + } + ); + } + + /** + * Get the user's life stage based on age. + */ + protected function lifeStage(): Attribute + { + return Attribute::make( + get: function () { + if (!$this->birthdate) { + return null; + } + + $age = Carbon::parse($this->birthdate)->age; + + if ($age >= 0 && $age <= 3) { + return 'Toddler'; + } elseif ($age >= 4 && $age <= 12) { + return 'Child'; + } elseif ($age >= 13 && $age <= 19) { + return 'Teenager'; + } elseif ($age >= 20 && $age <= 59) { + return 'Adult'; + } else { + return 'Senior'; + } + } + ); + } + + /** + * Get the dependents for the user. + */ + public function dependents(): HasMany + { + return $this->hasMany(UserRelationship::class, 'guardian_user_id'); + } + + /** + * Get the guardians for the user. + */ + public function guardians(): HasMany + { + return $this->hasMany(UserRelationship::class, 'dependent_user_id'); + } + + /** + * Get the clubs owned by the user. + */ + public function ownedClubs(): HasMany + { + return $this->hasMany(Tenant::class, 'owner_user_id'); + } + + /** + * Get the clubs the user is a member of. + */ + public function memberClubs(): BelongsToMany + { + return $this->belongsToMany(Tenant::class, 'memberships') + ->withPivot('status') + ->withTimestamps(); + } + + /** + * Get the invoices where the user is the student. + */ + public function studentInvoices(): HasMany + { + return $this->hasMany(Invoice::class, 'student_user_id'); + } + + /** + * Get the invoices where the user is the payer. + */ + public function payerInvoices(): HasMany + { + return $this->hasMany(Invoice::class, 'payer_user_id'); + } } diff --git a/app/Models/UserRelationship.php b/app/Models/UserRelationship.php new file mode 100644 index 0000000..1b09c44 --- /dev/null +++ b/app/Models/UserRelationship.php @@ -0,0 +1,49 @@ + + */ + protected $fillable = [ + 'guardian_user_id', + 'dependent_user_id', + 'relationship_type', + 'is_billing_contact', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'is_billing_contact' => 'boolean', + ]; + + /** + * Get the guardian user that owns the relationship. + */ + public function guardian(): BelongsTo + { + return $this->belongsTo(User::class, 'guardian_user_id'); + } + + /** + * Get the dependent user that belongs to the relationship. + */ + public function dependent(): BelongsTo + { + return $this->belongsTo(User::class, 'dependent_user_id'); + } +} diff --git a/app/Services/FamilyService.php b/app/Services/FamilyService.php new file mode 100644 index 0000000..7a42bc3 --- /dev/null +++ b/app/Services/FamilyService.php @@ -0,0 +1,58 @@ + $data['full_name'], + 'email' => $data['email'] ?? null, + 'password' => $data['password'] ?? null, + 'mobile' => $data['mobile'] ?? null, + 'gender' => $data['gender'], + 'birthdate' => $data['birthdate'], + 'blood_type' => $data['blood_type'] ?? null, + 'nationality' => $data['nationality'], + 'addresses' => $data['addresses'] ?? [], + 'social_links' => $data['social_links'] ?? [], + 'media_gallery' => $data['media_gallery'] ?? [], + ]); + + // Create the relationship between guardian and dependent + UserRelationship::create([ + 'guardian_user_id' => $guardian->id, + 'dependent_user_id' => $dependent->id, + 'relationship_type' => $data['relationship_type'], + 'is_billing_contact' => $data['is_billing_contact'] ?? false, + ]); + + return $dependent; + } + + /** + * Get all invoices where the guardian is the payer. + * + * @param int $guardianId The guardian user ID + * @return \Illuminate\Database\Eloquent\Collection The invoices + */ + public function getFamilyInvoices(int $guardianId) + { + return Invoice::where('payer_user_id', $guardianId) + ->with('student') + ->get(); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index c183276..c3928c5 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -7,6 +7,7 @@ use Illuminate\Foundation\Configuration\Middleware; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) diff --git a/composer.json b/composer.json index 44c6054..6ede6a4 100644 --- a/composer.json +++ b/composer.json @@ -8,6 +8,7 @@ "require": { "php": "^8.2", "laravel/framework": "^12.0", + "laravel/sanctum": "^4.0", "laravel/tinker": "^2.10.1" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 7dc845d..b41b38b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c514d8f7b9fc5970bdd94287905ef584", + "content-hash": "d3c16cb86c42230c6c023d9a5d9bcf42", "packages": [ { "name": "brick/math", @@ -1333,6 +1333,69 @@ }, "time": "2026-01-13T20:29:29+00:00" }, + { + "name": "laravel/sanctum", + "version": "v4.2.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "47d26f1d310879ff757b971f5a6fc631d18663fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/47d26f1d310879ff757b971f5a6fc631d18663fd", + "reference": "47d26f1d310879ff757b971f5a6fc631d18663fd", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^11.0|^12.0", + "illuminate/contracts": "^11.0|^12.0", + "illuminate/database": "^11.0|^12.0", + "illuminate/support": "^11.0|^12.0", + "php": "^8.2", + "symfony/console": "^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.15|^10.8", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2026-01-11T18:20:25+00:00" + }, { "name": "laravel/serializable-closure", "version": "v2.0.8", diff --git a/config/sanctum.php b/config/sanctum.php new file mode 100644 index 0000000..44527d6 --- /dev/null +++ b/config/sanctum.php @@ -0,0 +1,84 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort(), + // Sanctum::currentRequestHost(), + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + ], + +]; diff --git a/database/migrations/2026_01_20_064337_create_club_saas_schema.php b/database/migrations/2026_01_20_064337_create_club_saas_schema.php new file mode 100644 index 0000000..06d19f6 --- /dev/null +++ b/database/migrations/2026_01_20_064337_create_club_saas_schema.php @@ -0,0 +1,103 @@ +id(); + $table->string('full_name'); + $table->string('email')->nullable()->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('mobile')->nullable(); + $table->string('password')->nullable(); + $table->rememberToken(); + $table->enum('gender', ['male', 'female']); + $table->date('birthdate'); + $table->string('blood_type')->nullable(); + $table->string('nationality'); + $table->json('addresses'); + $table->json('social_links'); + $table->json('media_gallery'); + $table->timestamps(); + }); + + // 3. Create user_relationships table + Schema::create('user_relationships', function (Blueprint $table) { + $table->id(); + $table->foreignId('guardian_user_id')->constrained('users')->cascadeOnDelete(); + $table->foreignId('dependent_user_id')->constrained('users')->cascadeOnDelete(); + $table->string('relationship_type'); // son, daughter, spouse, sponsor, other + $table->boolean('is_billing_contact')->default(false); + $table->timestamps(); + }); + + // 4. Create tenants table (The Clubs) + Schema::create('tenants', function (Blueprint $table) { + $table->id(); + $table->foreignId('owner_user_id')->constrained('users')->cascadeOnDelete(); + $table->string('club_name'); + $table->string('slug')->unique(); + $table->string('logo')->nullable(); + $table->decimal('gps_lat', 10, 7)->nullable(); + $table->decimal('gps_long', 10, 7)->nullable(); + $table->timestamps(); + }); + + // 5. Create memberships table + Schema::create('memberships', function (Blueprint $table) { + $table->id(); + $table->foreignId('tenant_id')->constrained('tenants')->cascadeOnDelete(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->enum('status', ['active', 'inactive'])->default('active'); + $table->timestamps(); + }); + + // 6. Create invoices table + Schema::create('invoices', function (Blueprint $table) { + $table->id(); + $table->foreignId('tenant_id')->constrained('tenants')->cascadeOnDelete(); + $table->decimal('amount', 10, 2); + $table->string('status'); + $table->date('due_date'); + $table->foreignId('student_user_id')->constrained('users')->cascadeOnDelete(); + $table->foreignId('payer_user_id')->constrained('users')->cascadeOnDelete(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Drop tables in reverse order + Schema::dropIfExists('invoices'); + Schema::dropIfExists('memberships'); + Schema::dropIfExists('tenants'); + Schema::dropIfExists('user_relationships'); + Schema::dropIfExists('users'); + + // Recreate the original users table + Schema::create('users', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + } +}; diff --git a/database/migrations/2026_01_20_070433_create_personal_access_tokens_table.php b/database/migrations/2026_01_20_070433_create_personal_access_tokens_table.php new file mode 100644 index 0000000..40ff706 --- /dev/null +++ b/database/migrations/2026_01_20_070433_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->text('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/resources/views/family/create.blade.php b/resources/views/family/create.blade.php new file mode 100644 index 0000000..7e1fe50 --- /dev/null +++ b/resources/views/family/create.blade.php @@ -0,0 +1,101 @@ +@extends('layouts.app') + +@section('content') +
+
+
+
+
+

Add Family Member

+
+
+
+ @csrf + +
+ + + @error('full_name') +
{{ $message }}
+ @enderror +
+ +
+ + + @error('email') +
{{ $message }}
+ @enderror +
+ +
+
+ + + @error('gender') +
{{ $message }}
+ @enderror +
+
+ + + @error('birthdate') +
{{ $message }}
+ @enderror +
+
+ +
+
+ + + @error('blood_type') +
{{ $message }}
+ @enderror +
+
+ + + @error('nationality') +
{{ $message }}
+ @enderror +
+
+ +
+
+ + + @error('relationship_type') +
{{ $message }}
+ @enderror +
+
+ +
+ + +
+ +
+ Cancel + +
+
+
+
+
+
+
+@endsection diff --git a/resources/views/family/dashboard.blade.php b/resources/views/family/dashboard.blade.php new file mode 100644 index 0000000..e68025e --- /dev/null +++ b/resources/views/family/dashboard.blade.php @@ -0,0 +1,153 @@ +@extends('layouts.app') + +@section('content') +
+
+

My Family

+ +
+ + +
+ +
+
+
+
+ {{ $user->full_name }} +
+
{{ $user->full_name }}
+

+ Age: {{ $user->age }} ({{ $user->life_stage }}) +

+

+ {{ $user->horoscope }} +

+ + Edit Profile + +
+
+
+ + + @foreach($dependents as $relationship) +
+
+
+
+ {{ $relationship->dependent->full_name }} +
+
{{ $relationship->dependent->full_name }}
+

+ Age: {{ $relationship->dependent->age }} ({{ $relationship->dependent->life_stage }}) +

+ {{ ucfirst($relationship->relationship_type) }} +
+ +
+
+ @endforeach + + + +
+ + +
+
+

Family Payments

+
+
+
+ + + + + + + + + + + + @forelse($familyInvoices as $invoice) + + + + + + + + @empty + + + + @endforelse + +
Student NameClass/PackageAmountStatusActions
{{ $invoice->student->full_name }}{{ $invoice->tenant->club_name }}${{ number_format($invoice->amount, 2) }} + @if($invoice->status === 'paid') + Paid + @elseif($invoice->status === 'pending') + Pending + @else + Overdue + @endif + + + View + + @if($invoice->status !== 'paid') + + Pay + + @endif +
+

No payments due at this time.

+
+
+
+ +
+
+ + +@endsection diff --git a/resources/views/family/edit.blade.php b/resources/views/family/edit.blade.php new file mode 100644 index 0000000..2da9e45 --- /dev/null +++ b/resources/views/family/edit.blade.php @@ -0,0 +1,130 @@ +@extends('layouts.app') + +@section('content') +
+
+
+
+
+

Edit Family Member

+
+
+
+ @csrf + @method('PUT') + +
+ + + @error('full_name') +
{{ $message }}
+ @enderror +
+ +
+ + + @error('email') +
{{ $message }}
+ @enderror +
+ +
+
+ + + @error('gender') +
{{ $message }}
+ @enderror +
+
+ + + @error('birthdate') +
{{ $message }}
+ @enderror +
+
+ +
+
+ + + @error('blood_type') +
{{ $message }}
+ @enderror +
+
+ + + @error('nationality') +
{{ $message }}
+ @enderror +
+
+ +
+
+ + + @error('relationship_type') +
{{ $message }}
+ @enderror +
+
+ +
+ is_billing_contact) ? 'checked' : '' }}> + +
+ +
+ Cancel +
+ + +
+
+
+
+
+
+
+
+ + + +@endsection diff --git a/resources/views/family/show.blade.php b/resources/views/family/show.blade.php new file mode 100644 index 0000000..0e40b75 --- /dev/null +++ b/resources/views/family/show.blade.php @@ -0,0 +1,157 @@ +@extends('layouts.app') + +@section('content') +
+
+
+
+
+

Family Member Details

+ + Back to Family + +
+
+
+ {{ $relationship->dependent->full_name }} +

{{ $relationship->dependent->full_name }}

+ {{ ucfirst($relationship->relationship_type) }} + @if($relationship->is_billing_contact) + Billing Contact + @endif +
+ +
+
+
Age
+

{{ $relationship->dependent->age }} years ({{ $relationship->dependent->life_stage }})

+
+
+
Birthdate
+

{{ $relationship->dependent->birthdate->format('F j, Y') }}

+
+
+
Gender
+

{{ ucfirst($relationship->dependent->gender) }}

+
+
+
Horoscope
+

{{ $relationship->dependent->horoscope }}

+
+
+
Nationality
+

{{ $relationship->dependent->nationality }}

+
+
+
Blood Type
+

{{ $relationship->dependent->blood_type ?? 'Not specified' }}

+
+ @if($relationship->dependent->email) +
+
Email
+

{{ $relationship->dependent->email }}

+
+ @endif + @if($relationship->dependent->mobile) +
+
Mobile
+

{{ $relationship->dependent->mobile }}

+
+ @endif +
+ + +
+
+ + +
+
+
Club Memberships
+
+
+ @if($relationship->dependent->memberClubs->count() > 0) +
+ @foreach($relationship->dependent->memberClubs as $club) +
+
+
{{ $club->club_name }}
+ Status: {{ ucfirst($club->pivot->status) }} +
+ + {{ ucfirst($club->pivot->status) }} + +
+ @endforeach +
+ @else +

No club memberships found.

+ @endif +
+
+ + +
+
+
Recent Invoices
+
+
+ @if($relationship->dependent->studentInvoices->count() > 0) +
+ + + + + + + + + + + + @foreach($relationship->dependent->studentInvoices->take(5) as $invoice) + + + + + + + + @endforeach + +
ClubAmountStatusDue Date
{{ $invoice->tenant->club_name }}${{ number_format($invoice->amount, 2) }} + @if($invoice->status === 'paid') + Paid + @elseif($invoice->status === 'pending') + Pending + @else + Overdue + @endif + {{ $invoice->due_date->format('M j, Y') }} + + View + +
+
+ @if($relationship->dependent->studentInvoices->count() > 5) + + @endif + @else +

No invoices found.

+ @endif +
+
+
+
+
+@endsection diff --git a/resources/views/invoices/index.blade.php b/resources/views/invoices/index.blade.php new file mode 100644 index 0000000..e50cce1 --- /dev/null +++ b/resources/views/invoices/index.blade.php @@ -0,0 +1,97 @@ +@extends('layouts.app') + +@section('content') +
+
+

My Invoices

+
+ +
+
+

All Invoices

+
+ + +
+
+
+ @if(session('success')) + + @endif + + @if($invoices->count() > 0) +
+ + + + + + + + + + + + + + @foreach($invoices as $invoice) + + + + + + + + + + @endforeach + +
Invoice #StudentClubAmountStatusDue DateActions
{{ $invoice->id }}{{ $invoice->student->full_name }}{{ $invoice->tenant->club_name }}${{ number_format($invoice->amount, 2) }} + @if($invoice->status === 'paid') + Paid + @elseif($invoice->status === 'pending') + Pending + @else + Overdue + @endif + {{ $invoice->due_date->format('M j, Y') }} +
+ + View + + @if($invoice->status !== 'paid') + + Pay + + @endif +
+
+
+ @else +
+ +

No Invoices Found

+

There are no invoices matching your criteria.

+
+ @endif +
+ @if($invoices->where('status', '!=', 'paid')->count() > 0) + + @endif +
+
+@endsection diff --git a/resources/views/invoices/show.blade.php b/resources/views/invoices/show.blade.php new file mode 100644 index 0000000..3e533e3 --- /dev/null +++ b/resources/views/invoices/show.blade.php @@ -0,0 +1,100 @@ +@extends('layouts.app') + +@section('content') +
+
+
+
+
+

Invoice #{{ $invoice->id }}

+ + Back to Invoices + +
+
+ @if(session('success')) + + @endif + +
+
+
Billed To
+

{{ Auth::user()->full_name }}

+

{{ Auth::user()->email }}

+ @if(Auth::user()->mobile) +

{{ Auth::user()->mobile }}

+ @endif +
+
+
Invoice Details
+

Invoice #: {{ $invoice->id }}

+

Due Date: {{ $invoice->due_date->format('F j, Y') }}

+

+ Status: + @if($invoice->status === 'paid') + Paid + @elseif($invoice->status === 'pending') + Pending + @else + Overdue + @endif +

+
+
+ +
+
+
Club Information
+

{{ $invoice->tenant->club_name }}

+

{{ $invoice->tenant->owner->full_name }} (Owner)

+
+
+
Student Information
+

{{ $invoice->student->full_name }}

+

Age: {{ $invoice->student->age }} ({{ $invoice->student->life_stage }})

+
+
+ +
+ + + + + + + + + + + + + + + + + + + +
DescriptionAmount
Club Membership Fee - {{ $invoice->tenant->club_name }}${{ number_format($invoice->amount, 2) }}
Total${{ number_format($invoice->amount, 2) }}
+
+ +
+ @if($invoice->status !== 'paid') + + Pay Now + + @else + + @endif +
+
+
+
+
+
+@endsection diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php new file mode 100644 index 0000000..251829b --- /dev/null +++ b/resources/views/layouts/app.blade.php @@ -0,0 +1,128 @@ + + + + + + + + {{ config('app.name', 'Club SaaS') }} + + + + + + + + + + + + + + + + + + +
+ @yield('content') +
+ + + + + diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..ccc387f --- /dev/null +++ b/routes/api.php @@ -0,0 +1,8 @@ +user(); +})->middleware('auth:sanctum'); diff --git a/routes/web.php b/routes/web.php index 86a06c5..e075dd7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,7 +1,26 @@ group(function () { + Route::get('/family', [FamilyController::class, 'dashboard'])->name('family.dashboard'); + Route::get('/family/create', [FamilyController::class, 'create'])->name('family.create'); + Route::post('/family', [FamilyController::class, 'store'])->name('family.store'); + Route::get('/family/{id}', [FamilyController::class, 'show'])->name('family.show'); + Route::get('/family/{id}/edit', [FamilyController::class, 'edit'])->name('family.edit'); + Route::put('/family/{id}', [FamilyController::class, 'update'])->name('family.update'); + Route::delete('/family/{id}', [FamilyController::class, 'destroy'])->name('family.destroy'); + + // Invoice routes + Route::get('/invoices', [InvoiceController::class, 'index'])->name('invoices.index'); + Route::get('/invoices/{id}', [InvoiceController::class, 'show'])->name('invoices.show'); + Route::get('/invoices/{id}/pay', [InvoiceController::class, 'pay'])->name('invoices.pay'); + Route::get('/invoices/pay-all', [InvoiceController::class, 'payAll'])->name('invoices.pay-all'); +});