diff --git a/app/Console/Commands/CheckUserDetails.php b/app/Console/Commands/CheckUserDetails.php
new file mode 100644
index 0000000..e1f2007
--- /dev/null
+++ b/app/Console/Commands/CheckUserDetails.php
@@ -0,0 +1,59 @@
+argument('email');
+
+ $user = User::where('email', $email)->first();
+
+ if (!$user) {
+ $this->error("User with email {$email} not found");
+ return;
+ }
+
+ $this->info("User Details:");
+ $this->info("Name: {$user->name}");
+ $this->info("Email: {$user->email}");
+ $this->info("ID: {$user->id}");
+ $this->info("Status: {$user->status}");
+ $this->info("Password Hash: " . substr($user->password, 0, 30) . "...");
+ $this->info("Created: {$user->created_at}");
+ $this->info("Updated: {$user->updated_at}");
+
+ // Check customer relationship
+ $customer = $user->customer;
+ if ($customer) {
+ $this->info("Customer ID: {$customer->id}");
+ $this->info("Customer Name: {$customer->first_name} {$customer->last_name}");
+ $this->info("Customer Created: {$customer->created_at}");
+ } else {
+ $this->info("No associated customer found");
+ }
+
+ // Test a few common passwords
+ $testPasswords = ['test123', 'password', 'password123', '123456', 'admin123'];
+
+ $this->info("\nTesting common passwords:");
+ foreach ($testPasswords as $password) {
+ if (Hash::check($password, $user->password)) {
+ $this->info("✅ Password '{$password}' matches!");
+ return;
+ } else {
+ $this->line("❌ Password '{$password}' does not match");
+ }
+ }
+
+ $this->info("\nNone of the common passwords match.");
+ }
+}
diff --git a/app/Console/Commands/CheckUserRoles.php b/app/Console/Commands/CheckUserRoles.php
new file mode 100644
index 0000000..70f1905
--- /dev/null
+++ b/app/Console/Commands/CheckUserRoles.php
@@ -0,0 +1,49 @@
+argument('email');
+
+ $user = User::where('email', $email)->first();
+
+ if (!$user) {
+ $this->error("User with email {$email} not found");
+ return;
+ }
+
+ $this->info("User: {$user->name} (ID: {$user->id})");
+ $this->info("User Status: {$user->status}");
+ $this->info("isCustomer() result: " . ($user->isCustomer() ? 'true' : 'false'));
+
+ // Get detailed role information
+ $roles = DB::table('user_roles')
+ ->join('roles', 'user_roles.role_id', '=', 'roles.id')
+ ->where('user_roles.user_id', $user->id)
+ ->select('roles.name', 'user_roles.is_active', 'user_roles.assigned_at', 'user_roles.expires_at')
+ ->get();
+
+ $this->info("\nRole Details:");
+ if ($roles->isEmpty()) {
+ $this->info("No roles assigned");
+ } else {
+ foreach ($roles as $role) {
+ $this->info("Role: {$role->name}");
+ $this->info(" Active: " . ($role->is_active ? 'Yes' : 'No'));
+ $this->info(" Assigned: {$role->assigned_at}");
+ $this->info(" Expires: " . ($role->expires_at ?: 'Never'));
+ $this->info("");
+ }
+ }
+ }
+}
diff --git a/app/Console/Commands/CreateCustomerUser.php b/app/Console/Commands/CreateCustomerUser.php
new file mode 100644
index 0000000..8e2362c
--- /dev/null
+++ b/app/Console/Commands/CreateCustomerUser.php
@@ -0,0 +1,74 @@
+argument('email');
+ $name = $this->argument('name');
+ $password = $this->argument('password');
+
+ // Validate inputs
+ if (empty($name)) {
+ $this->error('Name cannot be empty');
+ return 1;
+ }
+
+ // Create or find customer record
+ $nameParts = explode(' ', $name, 2);
+ $firstName = $nameParts[0];
+ $lastName = $nameParts[1] ?? '';
+
+ // Check if customer already exists
+ $existingCustomer = Customer::where('email', $email)->first();
+ if ($existingCustomer) {
+ $this->info("Customer with email {$email} already exists.");
+ $customer = $existingCustomer;
+ } else {
+ $customer = Customer::create([
+ 'first_name' => $firstName,
+ 'last_name' => $lastName,
+ 'email' => $email,
+ 'phone' => '000-000-0000',
+ 'address' => '123 Main St',
+ 'city' => 'Anytown',
+ 'state' => 'CA',
+ 'zip_code' => '12345',
+ 'status' => 'active'
+ ]);
+ $this->info("Customer record created for {$email}");
+ }
+
+ // Check if user already exists
+ $existingUser = User::where('email', $email)->first();
+ if ($existingUser) {
+ $this->error("User with email {$email} already exists!");
+ return 1;
+ }
+
+ // Create user account
+ $user = User::create([
+ 'name' => $name,
+ 'email' => $email,
+ 'password' => Hash::make($password),
+ 'status' => 'active'
+ ]);
+
+ $this->info("Customer user created successfully!");
+ $this->info("Email: {$email}");
+ $this->info("Password: {$password}");
+ $this->info("You can now login at /login");
+
+ return 0;
+ }
+}
diff --git a/app/Console/Commands/CreateTestCustomer.php b/app/Console/Commands/CreateTestCustomer.php
new file mode 100644
index 0000000..dade23d
--- /dev/null
+++ b/app/Console/Commands/CreateTestCustomer.php
@@ -0,0 +1,68 @@
+delete();
+ User::where('email', $email)->delete();
+
+ // Create user
+ $user = User::create([
+ 'name' => 'Test Customer',
+ 'email' => $email,
+ 'password' => Hash::make($password),
+ 'phone' => '555-0123',
+ 'status' => 'active',
+ ]);
+
+ // Assign customer_portal role
+ $customerRole = Role::where('name', 'customer_portal')->first();
+ if ($customerRole) {
+ $user->roles()->attach($customerRole->id, [
+ 'is_active' => true,
+ 'assigned_at' => now(),
+ ]);
+ }
+
+ // Create customer
+ $customer = Customer::create([
+ 'user_id' => $user->id,
+ 'first_name' => 'Test',
+ 'last_name' => 'Customer',
+ 'email' => $email,
+ 'phone' => '555-0123',
+ 'address' => '123 Test St',
+ 'city' => 'Test City',
+ 'state' => 'CA',
+ 'zip_code' => '12345',
+ 'status' => 'active',
+ ]);
+
+ $this->info("Test customer created successfully!");
+ $this->info("Email: {$email}");
+ $this->info("Password: {$password}");
+ $this->info("User ID: {$user->id}");
+ $this->info("Customer ID: {$customer->id}");
+ });
+
+ return 0;
+ }
+}
diff --git a/app/Console/Commands/LinkCustomersToUsers.php b/app/Console/Commands/LinkCustomersToUsers.php
new file mode 100644
index 0000000..9ef4d05
--- /dev/null
+++ b/app/Console/Commands/LinkCustomersToUsers.php
@@ -0,0 +1,41 @@
+info('Linking existing customers to user accounts...');
+
+ $customers = Customer::whereNull('user_id')->get();
+ $linked = 0;
+ $created = 0;
+
+ foreach ($customers as $customer) {
+ // Find matching user by email
+ $user = User::where('email', $customer->email)->first();
+
+ if ($user) {
+ // Link existing user to customer
+ $customer->update(['user_id' => $user->id]);
+ $this->info("Linked customer {$customer->email} to existing user account");
+ $linked++;
+ } else {
+ $this->warn("No user account found for customer {$customer->email}");
+ }
+ }
+
+ $this->info("Migration completed:");
+ $this->info("- Linked {$linked} customers to existing user accounts");
+
+ return 0;
+ }
+}
diff --git a/app/Console/Commands/ResetUserPassword.php b/app/Console/Commands/ResetUserPassword.php
new file mode 100644
index 0000000..e245a74
--- /dev/null
+++ b/app/Console/Commands/ResetUserPassword.php
@@ -0,0 +1,39 @@
+argument('email');
+ $newPassword = $this->argument('password');
+
+ $user = User::where('email', $email)->first();
+
+ if (!$user) {
+ $this->error("User with email {$email} not found");
+ return;
+ }
+
+ $user->password = Hash::make($newPassword);
+ $user->save();
+
+ $this->info("Password reset successfully for {$user->name} ({$email})");
+ $this->info("New password: {$newPassword}");
+
+ // Test the new password immediately
+ if (Hash::check($newPassword, $user->fresh()->password)) {
+ $this->info("✅ Password verification successful!");
+ } else {
+ $this->error("❌ Password verification failed!");
+ }
+ }
+}
diff --git a/app/Console/Commands/SetupCustomerRoles.php b/app/Console/Commands/SetupCustomerRoles.php
new file mode 100644
index 0000000..fa8d96c
--- /dev/null
+++ b/app/Console/Commands/SetupCustomerRoles.php
@@ -0,0 +1,45 @@
+first();
+
+ if (!$customerRole) {
+ $this->error('customer_portal role not found!');
+ return 1;
+ }
+
+ // Find customer users
+ $customerEmails = ['customer@example.com', 'testcustomer@example.com'];
+ $users = User::whereIn('email', $customerEmails)->get();
+
+ foreach ($users as $user) {
+ if (!$user->hasRole('customer_portal')) {
+ $user->roles()->attach($customerRole->id, [
+ 'is_active' => true,
+ 'assigned_at' => now(),
+ 'branch_code' => null,
+ 'created_at' => now(),
+ 'updated_at' => now()
+ ]);
+ $this->info("Assigned customer_portal role to: {$user->email}");
+ } else {
+ $this->info("User {$user->email} already has customer_portal role");
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/app/Console/Commands/ShowCustomerUserIntegration.php b/app/Console/Commands/ShowCustomerUserIntegration.php
new file mode 100644
index 0000000..1190434
--- /dev/null
+++ b/app/Console/Commands/ShowCustomerUserIntegration.php
@@ -0,0 +1,63 @@
+info('=== CUSTOMER-USER INTEGRATION REPORT ===');
+ $this->newLine();
+
+ // Show customers with user accounts
+ $customersWithUsers = Customer::whereNotNull('user_id')->with('user')->get();
+ $this->info("Customers with User Accounts ({$customersWithUsers->count()}):");
+ foreach ($customersWithUsers as $customer) {
+ $this->line(" • {$customer->full_name} ({$customer->email}) → User ID: {$customer->user_id}");
+ }
+ $this->newLine();
+
+ // Show customers without user accounts
+ $customersWithoutUsers = Customer::whereNull('user_id')->get();
+ $this->warn("Customers without User Accounts ({$customersWithoutUsers->count()}):");
+ foreach ($customersWithoutUsers->take(5) as $customer) {
+ $this->line(" • {$customer->full_name} ({$customer->email})");
+ }
+ if ($customersWithoutUsers->count() > 5) {
+ $this->line(" ... and " . ($customersWithoutUsers->count() - 5) . " more");
+ }
+ $this->newLine();
+
+ // Show users with customer portal role
+ $customerUsers = User::whereHas('roles', function($q) {
+ $q->where('name', 'customer_portal');
+ })->with('customer')->get();
+
+ $this->info("Users with Customer Portal Role ({$customerUsers->count()}):");
+ foreach ($customerUsers as $user) {
+ $customerInfo = $user->customer ?
+ "→ Customer: {$user->customer->full_name}" :
+ "→ No Customer Record";
+ $this->line(" • {$user->name} ({$user->email}) {$customerInfo}");
+ }
+ $this->newLine();
+
+ // Show statistics
+ $this->info('=== STATISTICS ===');
+ $this->table(['Metric', 'Count'], [
+ ['Total Customers', Customer::count()],
+ ['Customers with User Accounts', Customer::whereNotNull('user_id')->count()],
+ ['Users with Customer Role', User::whereHas('roles', fn($q) => $q->where('name', 'customer_portal'))->count()],
+ ['Total Users', User::count()],
+ ]);
+
+ return 0;
+ }
+}
diff --git a/app/Console/Commands/TestCustomerCreation.php b/app/Console/Commands/TestCustomerCreation.php
new file mode 100644
index 0000000..d58f897
--- /dev/null
+++ b/app/Console/Commands/TestCustomerCreation.php
@@ -0,0 +1,113 @@
+info('Testing customer creation process...');
+
+ // Generate test data
+ $email = 'formtest' . time() . '@example.com';
+ $plainPassword = Str::random(12);
+ $firstName = 'Form';
+ $lastName = 'Test';
+ $phone = '555-0123';
+
+ $this->info("Creating customer with email: {$email}");
+ $this->info("Generated password: {$plainPassword}");
+
+ try {
+ DB::transaction(function () use ($email, $plainPassword, $firstName, $lastName, $phone) {
+ // Create user account exactly like the form
+ $user = User::create([
+ 'name' => $firstName . ' ' . $lastName,
+ 'email' => $email,
+ 'password' => Hash::make($plainPassword),
+ 'phone' => $phone,
+ 'status' => 'active',
+ ]);
+
+ $this->info("User created with ID: {$user->id}");
+ $this->info("Password hash: " . substr($user->password, 0, 20) . "...");
+
+ // Assign customer_portal role
+ $customerRole = Role::where('name', 'customer_portal')->first();
+ if ($customerRole) {
+ $user->roles()->attach($customerRole->id, [
+ 'is_active' => true,
+ 'assigned_at' => now(),
+ ]);
+ $this->info("Customer role assigned");
+ } else {
+ $this->error("Customer role not found!");
+ }
+
+ // Create customer record
+ $customer = Customer::create([
+ 'user_id' => $user->id,
+ 'first_name' => $firstName,
+ 'last_name' => $lastName,
+ 'email' => $email,
+ 'phone' => $phone,
+ 'address' => '123 Test St',
+ 'city' => 'Test City',
+ 'state' => 'CA',
+ 'zip_code' => '12345',
+ 'status' => 'active',
+ ]);
+
+ $this->info("Customer created with ID: {$customer->id}");
+ });
+
+ // Now test authentication immediately
+ $this->info("\n--- Testing authentication immediately ---");
+
+ $user = User::where('email', $email)->first();
+ if ($user) {
+ $this->info("User found: {$user->name} ({$user->email})");
+ $this->info("User ID: {$user->id}");
+ $this->info("User Status: {$user->status}");
+ $this->info("Password Hash: " . substr($user->password, 0, 30) . "...");
+
+ // Check password
+ if (Hash::check($plainPassword, $user->password)) {
+ $this->info("✅ Password matches!");
+ } else {
+ $this->error("❌ Password does NOT match!");
+ }
+
+ // Check roles
+ $roles = $user->roles()->where('user_roles.is_active', true)->pluck('name')->toArray();
+ $this->info("Roles: " . implode(', ', $roles));
+
+ // Test Laravel authentication
+ if (Auth::attempt(['email' => $email, 'password' => $plainPassword])) {
+ $this->info("✅ Authentication successful!");
+ Auth::logout(); // Clean up
+ } else {
+ $this->error("❌ Authentication failed!");
+ }
+ } else {
+ $this->error("User not found!");
+ }
+
+ } catch (\Exception $e) {
+ $this->error("Error: " . $e->getMessage());
+ $this->error("Trace: " . $e->getTraceAsString());
+ }
+ }
+}
diff --git a/app/Console/Commands/TestUserAuth.php b/app/Console/Commands/TestUserAuth.php
new file mode 100644
index 0000000..2a888f5
--- /dev/null
+++ b/app/Console/Commands/TestUserAuth.php
@@ -0,0 +1,52 @@
+argument('email');
+ $password = $this->argument('password');
+
+ $user = User::where('email', $email)->first();
+
+ if (!$user) {
+ $this->error("User with email {$email} not found!");
+ return 1;
+ }
+
+ $this->info("User found: {$user->name} ({$user->email})");
+ $this->info("User ID: {$user->id}");
+ $this->info("User Status: {$user->status}");
+ $this->info("Password Hash: " . substr($user->password, 0, 30) . "...");
+
+ // Test password
+ if (Hash::check($password, $user->password)) {
+ $this->info("✅ Password matches!");
+ } else {
+ $this->error("❌ Password does NOT match!");
+ }
+
+ // Check roles
+ $roles = $user->roles->pluck('name');
+ $this->info("Roles: " . $roles->join(', '));
+
+ // Test if user can login
+ if (auth()->attempt(['email' => $email, 'password' => $password])) {
+ $this->info("✅ Authentication successful!");
+ auth()->logout();
+ } else {
+ $this->error("❌ Authentication failed!");
+ }
+
+ return 0;
+ }
+}
diff --git a/app/Http/Middleware/AdminOnly.php b/app/Http/Middleware/AdminOnly.php
new file mode 100644
index 0000000..ae556b7
--- /dev/null
+++ b/app/Http/Middleware/AdminOnly.php
@@ -0,0 +1,31 @@
+user();
+
+ if (!$user) {
+ return redirect('/login');
+ }
+
+ // Check if user has customer role
+ if ($user->isCustomer()) {
+ // Customer accounts should only access customer portal
+ return redirect('/customer-portal');
+ }
+
+ // Allow admin/staff users to continue
+ return $next($request);
+ }
+}
diff --git a/app/Livewire/CustomerPortal/Appointments.php b/app/Livewire/CustomerPortal/Appointments.php
new file mode 100644
index 0000000..0c13127
--- /dev/null
+++ b/app/Livewire/CustomerPortal/Appointments.php
@@ -0,0 +1,51 @@
+resetPage();
+ }
+
+ public function updatingFilterDate()
+ {
+ $this->resetPage();
+ }
+
+ public function render()
+ {
+ $user = Auth::user();
+ $customer = Customer::where('email', $user->email)->first();
+
+ $appointments = collect();
+
+ if ($customer) {
+ $appointments = Appointment::where('customer_id', $customer->id)
+ ->when($this->filterStatus, function ($query) {
+ $query->where('status', $this->filterStatus);
+ })
+ ->when($this->filterDate, function ($query) {
+ $query->whereDate('scheduled_datetime', $this->filterDate);
+ })
+ ->with(['vehicle', 'assignedTechnician'])
+ ->orderBy('scheduled_datetime', 'desc')
+ ->paginate(10);
+ }
+
+ return view('livewire.customer-portal.appointments', compact('appointments'))
+ ->layout('layouts.customer-portal-app');
+ }
+}
diff --git a/app/Livewire/CustomerPortal/Dashboard.php b/app/Livewire/CustomerPortal/Dashboard.php
new file mode 100644
index 0000000..5c4158a
--- /dev/null
+++ b/app/Livewire/CustomerPortal/Dashboard.php
@@ -0,0 +1,143 @@
+customer;
+
+ if (!$customer) {
+ // If no customer record exists but user has customer role,
+ // create customer record linked to the user
+ if ($user->isCustomer()) {
+ $nameParts = explode(' ', $user->name, 2);
+ $firstName = $nameParts[0];
+ $lastName = $nameParts[1] ?? '';
+
+ $customer = \App\Models\Customer::create([
+ 'user_id' => $user->id,
+ 'first_name' => $firstName,
+ 'last_name' => $lastName,
+ 'email' => $user->email,
+ 'phone' => $user->phone ?? '',
+ 'address' => '',
+ 'city' => '',
+ 'state' => '',
+ 'zip_code' => '',
+ 'status' => 'active'
+ ]);
+ } else {
+ // User doesn't have customer role, redirect to dashboard
+ return redirect('/dashboard');
+ }
+ }
+
+ $this->loadDashboardData($customer);
+ }
+
+ private function loadDashboardData($customer)
+ {
+ // Load statistics
+ $this->stats = [
+ 'total_vehicles' => Vehicle::where('customer_id', $customer->id)->count(),
+ 'active_jobs' => JobCard::where('customer_id', $customer->id)
+ ->whereNotIn('status', ['completed', 'cancelled'])
+ ->count(),
+ 'pending_estimates' => Estimate::whereHas('jobCard', function($query) use ($customer) {
+ $query->where('customer_id', $customer->id);
+ })->whereIn('status', ['sent', 'viewed'])->count(),
+ 'completed_services' => JobCard::where('customer_id', $customer->id)
+ ->where('status', 'completed')
+ ->count(),
+ ];
+
+ // Load upcoming appointments
+ $this->upcomingAppointments = Appointment::where('customer_id', $customer->id)
+ ->where('scheduled_datetime', '>=', now())
+ ->orderBy('scheduled_datetime')
+ ->limit(5)
+ ->get();
+
+ // Load active job cards
+ $this->activeJobCards = JobCard::where('customer_id', $customer->id)
+ ->whereNotIn('status', ['completed', 'cancelled'])
+ ->with(['vehicle', 'serviceAdvisor'])
+ ->orderBy('created_at', 'desc')
+ ->limit(5)
+ ->get();
+
+ // Load recent activity
+ $this->recentActivity = collect()
+ ->merge(
+ JobCard::where('customer_id', $customer->id)
+ ->orderBy('updated_at', 'desc')
+ ->limit(3)
+ ->get()
+ ->map(function($jobCard) {
+ return [
+ 'type' => 'job_card',
+ 'title' => 'Job Card #' . $jobCard->id . ' ' . ucfirst(str_replace('_', ' ', $jobCard->status)),
+ 'description' => 'Vehicle: ' . ($jobCard->vehicle->year ?? '') . ' ' . ($jobCard->vehicle->make ?? '') . ' ' . ($jobCard->vehicle->model ?? ''),
+ 'date' => $jobCard->updated_at,
+ 'status' => $jobCard->status,
+ 'url' => route('customer-portal.status', $jobCard)
+ ];
+ })
+ )
+ ->merge(
+ Estimate::whereHas('jobCard', function($query) use ($customer) {
+ $query->where('customer_id', $customer->id);
+ })
+ ->orderBy('updated_at', 'desc')
+ ->limit(2)
+ ->get()
+ ->map(function($estimate) {
+ return [
+ 'type' => 'estimate',
+ 'title' => 'Estimate #' . $estimate->id . ' ' . ucfirst($estimate->status),
+ 'description' => '$' . number_format($estimate->total_amount, 2),
+ 'date' => $estimate->updated_at,
+ 'status' => $estimate->status,
+ 'url' => route('customer-portal.estimate', [$estimate->job_card_id, $estimate->id])
+ ];
+ })
+ )
+ ->sortByDesc('date')
+ ->take(5);
+ }
+
+ public function refreshDashboard()
+ {
+ $user = Auth::user();
+ $customer = \App\Models\Customer::where('email', $user->email)->first();
+
+ if ($customer) {
+ $this->loadDashboardData($customer);
+ session()->flash('message', 'Dashboard refreshed!');
+ }
+ }
+
+ public function render()
+ {
+ return view('livewire.customer-portal.dashboard')
+ ->layout('layouts.customer-portal-app');
+ }
+}
diff --git a/app/Livewire/CustomerPortal/Estimates.php b/app/Livewire/CustomerPortal/Estimates.php
new file mode 100644
index 0000000..c9c4b67
--- /dev/null
+++ b/app/Livewire/CustomerPortal/Estimates.php
@@ -0,0 +1,44 @@
+resetPage();
+ }
+
+ public function render()
+ {
+ $user = Auth::user();
+ $customer = Customer::where('email', $user->email)->first();
+
+ $estimates = collect();
+
+ if ($customer) {
+ $estimates = Estimate::whereHas('jobCard', function($query) use ($customer) {
+ $query->where('customer_id', $customer->id);
+ })
+ ->when($this->filterStatus, function ($query) {
+ $query->where('status', $this->filterStatus);
+ })
+ ->with(['jobCard.vehicle'])
+ ->orderBy('created_at', 'desc')
+ ->paginate(10);
+ }
+
+ return view('livewire.customer-portal.estimates', compact('estimates'))
+ ->layout('layouts.customer-portal-app');
+ }
+}
diff --git a/app/Livewire/CustomerPortal/Invoices.php b/app/Livewire/CustomerPortal/Invoices.php
new file mode 100644
index 0000000..8f26c7b
--- /dev/null
+++ b/app/Livewire/CustomerPortal/Invoices.php
@@ -0,0 +1,51 @@
+customer = Auth::user();
+ }
+
+ public function render()
+ {
+ $invoices = collect();
+
+ // Find customer by email
+ $customer = \App\Models\Customer::where('email', $this->customer->email)->first();
+
+ if ($customer) {
+ $query = ServiceOrder::with(['vehicle', 'customer'])
+ ->where('customer_id', $customer->id)
+ ->whereNotNull('completed_at')
+ ->orderBy('completed_at', 'desc');
+
+ if ($this->filterStatus) {
+ $query->where('status', $this->filterStatus);
+ }
+
+ $invoices = $query->paginate(10);
+ }
+
+ return view('livewire.customer-portal.invoices', [
+ 'invoices' => $invoices
+ ])->layout('layouts.customer-portal-app');
+ }
+
+ public function updatingFilterStatus()
+ {
+ $this->resetPage();
+ }
+}
diff --git a/app/Livewire/CustomerPortal/Vehicles.php b/app/Livewire/CustomerPortal/Vehicles.php
new file mode 100644
index 0000000..9a17dcb
--- /dev/null
+++ b/app/Livewire/CustomerPortal/Vehicles.php
@@ -0,0 +1,29 @@
+email)->first();
+
+ $vehicles = collect();
+
+ if ($customer) {
+ $vehicles = Vehicle::where('customer_id', $customer->id)
+ ->withCount(['jobCards', 'appointments'])
+ ->orderBy('created_at', 'desc')
+ ->get();
+ }
+
+ return view('livewire.customer-portal.vehicles', compact('vehicles'))
+ ->layout('layouts.customer-portal-app');
+ }
+}
diff --git a/app/Livewire/CustomerPortal/WorkOrders.php b/app/Livewire/CustomerPortal/WorkOrders.php
new file mode 100644
index 0000000..85e5574
--- /dev/null
+++ b/app/Livewire/CustomerPortal/WorkOrders.php
@@ -0,0 +1,42 @@
+resetPage();
+ }
+
+ public function render()
+ {
+ $user = Auth::user();
+ $customer = Customer::where('email', $user->email)->first();
+
+ $workOrders = collect();
+
+ if ($customer) {
+ $workOrders = JobCard::where('customer_id', $customer->id)
+ ->when($this->filterStatus, function ($query) {
+ $query->where('status', $this->filterStatus);
+ })
+ ->with(['vehicle', 'serviceAdvisor', 'estimates'])
+ ->orderBy('created_at', 'desc')
+ ->paginate(10);
+ }
+
+ return view('livewire.customer-portal.work-orders', compact('workOrders'))
+ ->layout('layouts.customer-portal-app');
+ }
+}
diff --git a/app/Livewire/Customers/Create.php b/app/Livewire/Customers/Create.php
index de03f9d..3033d5f 100644
--- a/app/Livewire/Customers/Create.php
+++ b/app/Livewire/Customers/Create.php
@@ -3,10 +3,16 @@
namespace App\Livewire\Customers;
use App\Models\Customer;
+use App\Models\User;
+use App\Models\Role;
use Livewire\Component;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Str;
class Create extends Component
{
+ // Customer Information
public $first_name = '';
public $last_name = '';
public $email = '';
@@ -19,10 +25,15 @@ class Create extends Component
public $notes = '';
public $status = 'active';
+ // User Account Options
+ public $create_user_account = true;
+ public $password = '';
+ public $send_welcome_email = true;
+
protected $rules = [
'first_name' => 'required|string|max:255',
'last_name' => 'required|string|max:255',
- 'email' => 'required|email|unique:customers,email',
+ 'email' => 'required|email|unique:customers,email|unique:users,email',
'phone' => 'required|string|max:255',
'secondary_phone' => 'nullable|string|max:255',
'address' => 'required|string|max:500',
@@ -31,6 +42,7 @@ class Create extends Component
'zip_code' => 'required|string|max:10',
'notes' => 'nullable|string|max:1000',
'status' => 'required|in:active,inactive',
+ 'password' => 'required_if:create_user_account,true|min:8|nullable',
];
protected $messages = [
@@ -44,28 +56,74 @@ class Create extends Component
'city.required' => 'City is required.',
'state.required' => 'State is required.',
'zip_code.required' => 'ZIP code is required.',
+ 'password.required_if' => 'Password is required when creating a user account.',
+ 'password.min' => 'Password must be at least 8 characters.',
];
+ public function mount()
+ {
+ // Generate a random password by default
+ $this->password = Str::random(12);
+ }
+
+ public function generatePassword()
+ {
+ $this->password = Str::random(12);
+ }
+
public function save()
{
$this->validate();
- $customer = Customer::create([
- 'first_name' => $this->first_name,
- 'last_name' => $this->last_name,
- 'email' => $this->email,
- 'phone' => $this->phone,
- 'secondary_phone' => $this->secondary_phone,
- 'address' => $this->address,
- 'city' => $this->city,
- 'state' => $this->state,
- 'zip_code' => $this->zip_code,
- 'notes' => $this->notes,
- 'status' => $this->status,
- ]);
+ DB::transaction(function () {
+ $user = null;
+
+ // Create user account if requested
+ if ($this->create_user_account) {
+ $user = User::create([
+ 'name' => $this->first_name . ' ' . $this->last_name,
+ 'email' => $this->email,
+ 'password' => Hash::make($this->password),
+ 'phone' => $this->phone,
+ 'status' => 'active',
+ ]);
- session()->flash('success', 'Customer created successfully!');
- return redirect()->route('customers.show', $customer);
+ // Assign customer_portal role
+ $customerRole = Role::where('name', 'customer_portal')->first();
+ if ($customerRole) {
+ $user->roles()->attach($customerRole->id, [
+ 'is_active' => true,
+ 'assigned_at' => now(),
+ ]);
+ }
+ }
+
+ // Create customer record
+ $customer = Customer::create([
+ 'user_id' => $user?->id,
+ 'first_name' => $this->first_name,
+ 'last_name' => $this->last_name,
+ 'email' => $this->email,
+ 'phone' => $this->phone,
+ 'secondary_phone' => $this->secondary_phone,
+ 'address' => $this->address,
+ 'city' => $this->city,
+ 'state' => $this->state,
+ 'zip_code' => $this->zip_code,
+ 'notes' => $this->notes,
+ 'status' => $this->status,
+ ]);
+
+ // TODO: Send welcome email if requested
+ if ($this->create_user_account && $this->send_welcome_email) {
+ // Implement welcome email logic here
+ }
+
+ session()->flash('success', 'Customer created successfully!' .
+ ($this->create_user_account ? ' User account also created with password: ' . $this->password : ''));
+
+ $this->redirect(route('customers.show', $customer), navigate: true);
+ });
}
public function render()
diff --git a/app/Livewire/Customers/Edit.php b/app/Livewire/Customers/Edit.php
index e6d4517..df2b3d2 100644
--- a/app/Livewire/Customers/Edit.php
+++ b/app/Livewire/Customers/Edit.php
@@ -4,48 +4,56 @@ namespace App\Livewire\Customers;
use Livewire\Component;
use App\Models\Customer;
-use Livewire\Attributes\Validate;
+use App\Models\User;
+use App\Models\Role;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Str;
class Edit extends Component
{
public Customer $customer;
- #[Validate('required|string|max:255')]
+ // Customer fields
public $first_name = '';
-
- #[Validate('required|string|max:255')]
public $last_name = '';
-
- #[Validate('required|email|max:255|unique:customers,email')]
public $email = '';
-
- #[Validate('required|string|max:20')]
public $phone = '';
-
- #[Validate('nullable|string|max:20')]
public $secondary_phone = '';
-
- #[Validate('required|string|max:500')]
public $address = '';
-
- #[Validate('required|string|max:255')]
public $city = '';
-
- #[Validate('required|string|max:255')]
public $state = '';
-
- #[Validate('required|string|max:10')]
public $zip_code = '';
-
- #[Validate('nullable|string|max:1000')]
public $notes = '';
-
- #[Validate('required|in:active,inactive')]
public $status = 'active';
+ // User account management
+ public $has_user_account = false;
+ public $create_user_account = false;
+ public $reset_password = false;
+ public $new_password = '';
+ public $user_status = 'active';
+
+ protected $rules = [
+ 'first_name' => 'required|string|max:255',
+ 'last_name' => 'required|string|max:255',
+ 'email' => 'required|email|max:255',
+ 'phone' => 'required|string|max:20',
+ 'secondary_phone' => 'nullable|string|max:20',
+ 'address' => 'required|string|max:500',
+ 'city' => 'required|string|max:255',
+ 'state' => 'required|string|max:255',
+ 'zip_code' => 'required|string|max:10',
+ 'notes' => 'nullable|string|max:1000',
+ 'status' => 'required|in:active,inactive',
+ 'new_password' => 'required_if:reset_password,true|min:8|nullable',
+ ];
+
public function mount(Customer $customer)
{
- $this->customer = $customer;
+ $this->customer = $customer->load('user');
+
+ // Load customer data
$this->first_name = $customer->first_name;
$this->last_name = $customer->last_name;
$this->email = $customer->email;
@@ -57,40 +65,119 @@ class Edit extends Component
$this->zip_code = $customer->zip_code;
$this->notes = $customer->notes;
$this->status = $customer->status;
+
+ // Check if customer has user account (regardless of role status)
+ $this->has_user_account = $customer->user !== null;
+
+ if ($customer->user) {
+ $this->user_status = $customer->user->status;
+ }
+
+ // Generate random password for reset
+ $this->new_password = Str::random(12);
+ }
+
+ public function generatePassword()
+ {
+ $this->new_password = Str::random(12);
}
public function updateCustomer()
{
// Update validation rules to exclude current customer's email
- $this->validate([
- 'first_name' => 'required|string|max:255',
- 'last_name' => 'required|string|max:255',
- 'email' => 'required|email|max:255|unique:customers,email,' . $this->customer->id,
- 'phone' => 'required|string|max:20',
- 'secondary_phone' => 'nullable|string|max:20',
- 'address' => 'required|string|max:500',
- 'city' => 'required|string|max:255',
- 'state' => 'required|string|max:255',
- 'zip_code' => 'required|string|max:10',
- 'notes' => 'nullable|string|max:1000',
- 'status' => 'required|in:active,inactive',
- ]);
+ $rules = $this->rules;
+ $rules['email'] = 'required|email|max:255|unique:customers,email,' . $this->customer->id;
+
+ if ($this->create_user_account && !$this->has_user_account) {
+ $rules['email'] .= '|unique:users,email';
+ }
- $this->customer->update([
- 'first_name' => $this->first_name,
- 'last_name' => $this->last_name,
- 'email' => $this->email,
- 'phone' => $this->phone,
- 'secondary_phone' => $this->secondary_phone,
- 'address' => $this->address,
- 'city' => $this->city,
- 'state' => $this->state,
- 'zip_code' => $this->zip_code,
- 'notes' => $this->notes,
- 'status' => $this->status,
- ]);
+ $this->validate($rules);
- session()->flash('success', 'Customer updated successfully!');
+ DB::transaction(function () {
+ // Update customer record
+ $this->customer->update([
+ 'first_name' => $this->first_name,
+ 'last_name' => $this->last_name,
+ 'email' => $this->email,
+ 'phone' => $this->phone,
+ 'secondary_phone' => $this->secondary_phone,
+ 'address' => $this->address,
+ 'city' => $this->city,
+ 'state' => $this->state,
+ 'zip_code' => $this->zip_code,
+ 'notes' => $this->notes,
+ 'status' => $this->status,
+ ]);
+
+ // Handle user account creation
+ if ($this->create_user_account && !$this->has_user_account) {
+ $user = User::create([
+ 'name' => $this->first_name . ' ' . $this->last_name,
+ 'email' => $this->email,
+ 'password' => Hash::make($this->new_password),
+ 'phone' => $this->phone,
+ 'status' => 'active',
+ ]);
+
+ // Assign customer_portal role
+ $customerRole = Role::where('name', 'customer_portal')->first();
+ if ($customerRole) {
+ $user->roles()->attach($customerRole->id, [
+ 'is_active' => true,
+ 'assigned_at' => now(),
+ ]);
+ }
+
+ // Link customer to user
+ $this->customer->update(['user_id' => $user->id]);
+ }
+
+ // Handle existing user account updates
+ if ($this->has_user_account && $this->customer->user) {
+ $userUpdates = [
+ 'name' => $this->first_name . ' ' . $this->last_name,
+ 'email' => $this->email,
+ 'phone' => $this->phone,
+ 'status' => $this->user_status,
+ ];
+
+ if ($this->reset_password) {
+ $userUpdates['password'] = Hash::make($this->new_password);
+ }
+
+ $this->customer->user->update($userUpdates);
+
+ // Update customer_portal role activation based on user status
+ $customerRole = Role::where('name', 'customer_portal')->first();
+ if ($customerRole) {
+ // Check if user has the role
+ $existingRole = $this->customer->user->roles()->where('role_id', $customerRole->id)->first();
+
+ if ($existingRole) {
+ // Update the role's is_active status based on user_status
+ $this->customer->user->roles()->updateExistingPivot($customerRole->id, [
+ 'is_active' => $this->user_status === 'active'
+ ]);
+ } elseif ($this->user_status === 'active') {
+ // If role doesn't exist and user should be active, add it
+ $this->customer->user->roles()->attach($customerRole->id, [
+ 'is_active' => true,
+ 'assigned_at' => now(),
+ ]);
+ }
+ }
+ }
+ });
+
+ $message = 'Customer updated successfully!';
+ if ($this->create_user_account && !$this->has_user_account) {
+ $message .= ' User account created with password: ' . $this->new_password;
+ } elseif ($this->reset_password) {
+ $message .= ' Password reset to: ' . $this->new_password;
+ }
+
+ session()->flash('success', $message);
return $this->redirect('/customers/' . $this->customer->id, navigate: true);
}
diff --git a/app/Livewire/Customers/Index.php b/app/Livewire/Customers/Index.php
index 1136b3e..69a096f 100644
--- a/app/Livewire/Customers/Index.php
+++ b/app/Livewire/Customers/Index.php
@@ -53,15 +53,21 @@ class Index extends Component
}
$customerName = $customer->full_name;
+
+ // Also delete associated user account if exists
+ if ($customer->user) {
+ $customer->user->delete();
+ }
+
$customer->delete();
- session()->flash('success', "Customer {$customerName} has been deleted successfully.");
+ session()->flash('success', "Customer {$customerName} and associated user account have been deleted successfully.");
}
public function render()
{
$customers = Customer::query()
- ->with(['vehicles'])
+ ->with(['vehicles', 'user'])
->when($this->search, function ($query) {
$query->where(function ($q) {
$q->where('first_name', 'like', '%' . $this->search . '%')
diff --git a/app/Livewire/Customers/Show.php b/app/Livewire/Customers/Show.php
index 3642746..52a011e 100644
--- a/app/Livewire/Customers/Show.php
+++ b/app/Livewire/Customers/Show.php
@@ -8,10 +8,32 @@ use Livewire\Component;
class Show extends Component
{
public Customer $customer;
+ public $stats = [];
public function mount(Customer $customer)
{
- $this->customer = $customer->load(['vehicles', 'serviceOrders.assignedTechnician', 'appointments']);
+ $this->customer = $customer->load([
+ 'user',
+ 'vehicles.jobCards',
+ 'serviceOrders.assignedTechnician',
+ 'appointments'
+ ]);
+
+ $this->loadStats();
+ }
+
+ private function loadStats()
+ {
+ $this->stats = [
+ 'total_vehicles' => $this->customer->vehicles->count(),
+ 'total_service_orders' => $this->customer->serviceOrders->count(),
+ 'total_appointments' => $this->customer->appointments->count(),
+ 'active_job_cards' => $this->customer->vehicles->sum(function($vehicle) {
+ return $vehicle->jobCards->where('status', '!=', 'completed')->count();
+ }),
+ 'completed_services' => $this->customer->serviceOrders->where('status', 'completed')->count(),
+ 'total_spent' => $this->customer->serviceOrders->where('status', 'completed')->sum('total_amount'),
+ ];
}
public function render()
diff --git a/app/Livewire/Timesheets/Index.php b/app/Livewire/Timesheets/Index.php
index b2e679c..a10e5ee 100644
--- a/app/Livewire/Timesheets/Index.php
+++ b/app/Livewire/Timesheets/Index.php
@@ -3,54 +3,203 @@
namespace App\Livewire\Timesheets;
use App\Models\Timesheet;
+use App\Models\JobCard;
use Livewire\Component;
use Livewire\WithPagination;
+use Livewire\Attributes\Validate;
class Index extends Component
{
use WithPagination;
- public $search = '';
- public $typeFilter = '';
- public $statusFilter = '';
- public $dateFilter = '';
+ // Filter properties
+ public $selectedTechnician = '';
+ public $dateFrom = '';
+ public $dateTo = '';
+ public $selectedStatus = '';
- public function updatingSearch()
+ // Modal properties
+ public $showCreateModal = false;
+ public $showEditModal = false;
+ public $editingTimesheetId = null;
+
+ // Form properties
+ public $form = [
+ 'user_id' => '',
+ 'job_card_id' => '',
+ 'date' => '',
+ 'start_time' => '',
+ 'end_time' => '',
+ 'description' => '',
+ ];
+
+ public function mount()
+ {
+ $this->dateFrom = now()->startOfMonth()->format('Y-m-d');
+ $this->dateTo = now()->format('Y-m-d');
+ $this->form['date'] = now()->format('Y-m-d');
+ }
+
+ public function updatingSelectedTechnician()
{
$this->resetPage();
}
+ public function updatingDateFrom()
+ {
+ $this->resetPage();
+ }
+
+ public function updatingDateTo()
+ {
+ $this->resetPage();
+ }
+
+ public function updatingSelectedStatus()
+ {
+ $this->resetPage();
+ }
+
+ public function createTimesheet()
+ {
+ $this->validate([
+ 'form.user_id' => 'required|exists:users,id',
+ 'form.date' => 'required|date',
+ 'form.start_time' => 'required',
+ 'form.end_time' => 'nullable|after:form.start_time',
+ 'form.description' => 'nullable|string|max:1000',
+ ]);
+
+ $timesheet = new Timesheet();
+ $timesheet->user_id = $this->form['user_id'];
+ $timesheet->job_card_id = $this->form['job_card_id'] ?: null;
+ $timesheet->date = $this->form['date'];
+ $timesheet->start_time = $this->form['date'] . ' ' . $this->form['start_time'];
+
+ if ($this->form['end_time']) {
+ $timesheet->end_time = $this->form['date'] . ' ' . $this->form['end_time'];
+ $timesheet->hours_worked = $this->calculateHours($this->form['start_time'], $this->form['end_time']);
+ $timesheet->status = 'completed';
+ } else {
+ $timesheet->status = 'active';
+ $timesheet->hours_worked = 0;
+ }
+
+ $timesheet->description = $this->form['description'];
+ $timesheet->save();
+
+ $this->closeModal();
+ session()->flash('message', 'Timesheet entry created successfully.');
+ }
+
+ public function editTimesheet($id)
+ {
+ $timesheet = Timesheet::findOrFail($id);
+ $this->editingTimesheetId = $id;
+ $this->form = [
+ 'user_id' => $timesheet->user_id,
+ 'job_card_id' => $timesheet->job_card_id,
+ 'date' => $timesheet->date->format('Y-m-d'),
+ 'start_time' => $timesheet->start_time ? $timesheet->start_time->format('H:i') : '',
+ 'end_time' => $timesheet->end_time ? $timesheet->end_time->format('H:i') : '',
+ 'description' => $timesheet->description,
+ ];
+ $this->showEditModal = true;
+ }
+
+ public function updateTimesheet()
+ {
+ $this->validate([
+ 'form.user_id' => 'required|exists:users,id',
+ 'form.date' => 'required|date',
+ 'form.start_time' => 'required',
+ 'form.end_time' => 'nullable|after:form.start_time',
+ 'form.description' => 'nullable|string|max:1000',
+ ]);
+
+ $timesheet = Timesheet::findOrFail($this->editingTimesheetId);
+ $timesheet->user_id = $this->form['user_id'];
+ $timesheet->job_card_id = $this->form['job_card_id'] ?: null;
+ $timesheet->date = $this->form['date'];
+ $timesheet->start_time = $this->form['date'] . ' ' . $this->form['start_time'];
+
+ if ($this->form['end_time']) {
+ $timesheet->end_time = $this->form['date'] . ' ' . $this->form['end_time'];
+ $timesheet->hours_worked = $this->calculateHours($this->form['start_time'], $this->form['end_time']);
+ $timesheet->status = 'submitted';
+ } else {
+ $timesheet->end_time = null;
+ $timesheet->status = 'draft';
+ $timesheet->hours_worked = 0;
+ }
+
+ $timesheet->description = $this->form['description'];
+ $timesheet->save();
+
+ $this->closeModal();
+ session()->flash('message', 'Timesheet entry updated successfully.');
+ }
+
+ public function deleteTimesheet($id)
+ {
+ Timesheet::findOrFail($id)->delete();
+ session()->flash('message', 'Timesheet entry deleted successfully.');
+ }
+
+ public function closeModal()
+ {
+ $this->showCreateModal = false;
+ $this->showEditModal = false;
+ $this->editingTimesheetId = null;
+ $this->form = [
+ 'user_id' => '',
+ 'job_card_id' => '',
+ 'date' => now()->format('Y-m-d'),
+ 'start_time' => '',
+ 'end_time' => '',
+ 'description' => '',
+ ];
+ }
+
+ private function calculateHours($startTime, $endTime)
+ {
+ $start = \Carbon\Carbon::createFromFormat('H:i', $startTime);
+ $end = \Carbon\Carbon::createFromFormat('H:i', $endTime);
+ return $end->diffInHours($start, true);
+ }
+
public function render()
{
$timesheets = Timesheet::with([
+ 'user',
'jobCard.customer',
- 'jobCard.vehicle',
- 'technician',
- 'workOrderTask'
+ 'jobCard.vehicle'
])
- ->when($this->search, function ($query) {
- $query->whereHas('jobCard', function ($jobQuery) {
- $jobQuery->where('job_number', 'like', '%' . $this->search . '%')
- ->orWhereHas('customer', function ($customerQuery) {
- $customerQuery->where('name', 'like', '%' . $this->search . '%');
- });
- })
- ->orWhereHas('technician', function ($techQuery) {
- $techQuery->where('name', 'like', '%' . $this->search . '%');
- });
+ ->when($this->selectedTechnician, function ($query) {
+ $query->where('user_id', $this->selectedTechnician);
})
- ->when($this->typeFilter, function ($query) {
- $query->where('task_type', $this->typeFilter);
+ ->when($this->dateFrom, function ($query) {
+ $query->whereDate('date', '>=', $this->dateFrom);
})
- ->when($this->statusFilter, function ($query) {
- $query->where('status', $this->statusFilter);
+ ->when($this->dateTo, function ($query) {
+ $query->whereDate('date', '<=', $this->dateTo);
})
- ->when($this->dateFilter, function ($query) {
- $query->whereDate('start_time', $this->dateFilter);
+ ->when($this->selectedStatus, function ($query) {
+ $query->where('status', $this->selectedStatus);
})
- ->latest()
+ ->latest('date')
->paginate(15);
- return view('livewire.timesheets.index', compact('timesheets'));
+ $technicians = \App\Models\User::where('role', 'technician')
+ ->orWhere('role', 'admin')
+ ->orderBy('name')
+ ->get();
+ $jobCards = JobCard::with('vehicle', 'customer')
+ ->where('status', '!=', 'completed')
+ ->latest()
+ ->limit(50)
+ ->get();
+
+ return view('livewire.timesheets.index', compact('timesheets', 'technicians', 'jobCards'));
}
}
diff --git a/app/Livewire/Users/Index.php b/app/Livewire/Users/Index.php
index 2db549f..b038b70 100644
--- a/app/Livewire/Users/Index.php
+++ b/app/Livewire/Users/Index.php
@@ -16,6 +16,7 @@ class Index extends Component
public $statusFilter = '';
public $departmentFilter = '';
public $branchFilter = '';
+ public $customerFilter = '';
public $sortField = 'name';
public $sortDirection = 'asc';
public $perPage = 25;
@@ -29,6 +30,7 @@ class Index extends Component
'statusFilter' => ['except' => ''],
'departmentFilter' => ['except' => ''],
'branchFilter' => ['except' => ''],
+ 'customerFilter' => ['except' => ''],
'sortField' => ['except' => 'name'],
'sortDirection' => ['except' => 'asc'],
'perPage' => ['except' => 25],
@@ -60,6 +62,11 @@ class Index extends Component
$this->resetPage();
}
+ public function updatingCustomerFilter()
+ {
+ $this->resetPage();
+ }
+
public function updatingPerPage()
{
$this->resetPage();
@@ -74,7 +81,7 @@ class Index extends Component
$q->whereNull('user_roles.expires_at')
->orWhere('user_roles.expires_at', '>', now());
});
- }])
+ }, 'customer'])
->withCount(['roles as active_roles_count' => function($query) {
$query->where('user_roles.is_active', true)
->where(function ($q) {
@@ -110,6 +117,13 @@ class Index extends Component
->when($this->branchFilter, function ($q) {
$q->where('branch_code', $this->branchFilter);
})
+ ->when($this->customerFilter, function ($q) {
+ if ($this->customerFilter === 'customers_only') {
+ $q->whereHas('customer');
+ } elseif ($this->customerFilter === 'non_customers') {
+ $q->whereDoesntHave('customer');
+ }
+ })
->when(!$this->showInactive, function ($q) {
$q->where('status', '!=', 'inactive');
})
@@ -137,6 +151,8 @@ class Index extends Component
'active' => User::where('status', 'active')->count(),
'inactive' => User::where('status', 'inactive')->count(),
'suspended' => User::where('status', 'suspended')->count(),
+ 'customers' => User::whereHas('customer')->count(),
+ 'staff' => User::whereDoesntHave('customer')->count(),
];
return view('livewire.users.index', compact('users', 'roles', 'departments', 'branches', 'stats'));
@@ -159,8 +175,7 @@ class Index extends Component
$this->statusFilter = '';
$this->departmentFilter = '';
$this->branchFilter = '';
- $this->sortField = 'name';
- $this->sortDirection = 'asc';
+ $this->customerFilter = '';
$this->showInactive = false;
$this->resetPage();
}
@@ -319,6 +334,7 @@ class Index extends Component
!empty($this->statusFilter) ||
!empty($this->departmentFilter) ||
!empty($this->branchFilter) ||
+ !empty($this->customerFilter) ||
$this->showInactive;
}
@@ -359,6 +375,7 @@ class Index extends Component
'parts_clerk' => 'bg-amber-100 dark:bg-amber-900 text-amber-800 dark:text-amber-200',
'service_advisor' => 'bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200',
'cashier' => 'bg-pink-100 dark:bg-pink-900 text-pink-800 dark:text-pink-200',
+ 'customer_portal' => 'bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200',
'customer' => 'bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200',
default => 'bg-zinc-100 dark:bg-zinc-700 text-zinc-800 dark:text-zinc-200',
};
diff --git a/app/Livewire/Users/ManageRolesPermissions.php b/app/Livewire/Users/ManageRolesPermissions.php
index ea1bd2a..4dc17d8 100644
--- a/app/Livewire/Users/ManageRolesPermissions.php
+++ b/app/Livewire/Users/ManageRolesPermissions.php
@@ -34,7 +34,7 @@ class ManageRolesPermissions extends Component
public function mount(User $user)
{
- $this->user = $user;
+ $this->user = $user->load('customer');
$this->branchCode = $user->branch_code ?? auth()->user()->branch_code ?? '';
// Load current roles
@@ -314,6 +314,10 @@ class ManageRolesPermissions extends Component
'parts_clerk' => [
'roles' => ['parts_manager'],
'permissions' => [] // Parts clerk gets permissions through role
+ ],
+ 'customer_portal' => [
+ 'roles' => ['customer_portal'],
+ 'permissions' => [] // Customer portal gets basic customer permissions through role
]
];
@@ -336,7 +340,7 @@ class ManageRolesPermissions extends Component
$this->selectedPermissions = [];
}
- session()->flash('success', 'Applied ' . ucfirst($roleType) . ' preset successfully.');
+ session()->flash('success', 'Applied ' . ucfirst(str_replace('_', ' ', $roleType)) . ' preset successfully.');
}
public function selectAllPermissions()
@@ -390,4 +394,19 @@ class ManageRolesPermissions extends Component
session()->flash('error', 'Failed to remove permissions: ' . $e->getMessage());
}
}
+
+ public function isCustomerPortalUser()
+ {
+ return $this->user->customer !== null;
+ }
+
+ public function getRecommendedRoleForCustomer()
+ {
+ return $this->user->customer ? 'customer_portal' : null;
+ }
+
+ public function hasCustomerPortalRole()
+ {
+ return $this->user->hasRole('customer_portal');
+ }
}
diff --git a/app/Livewire/Users/Show.php b/app/Livewire/Users/Show.php
index deeb6a1..3ecf997 100644
--- a/app/Livewire/Users/Show.php
+++ b/app/Livewire/Users/Show.php
@@ -29,7 +29,7 @@ class Show extends Component
public function mount(User $user)
{
- $this->user = $user->load(['roles.permissions', 'permissions']);
+ $this->user = $user->load(['roles.permissions', 'permissions', 'customer']);
}
public function render()
@@ -452,6 +452,7 @@ class Show extends Component
'service_advisor' => 'bg-teal-100 dark:bg-teal-900 text-teal-800 dark:text-teal-200',
'receptionist' => 'bg-pink-100 dark:bg-pink-900 text-pink-800 dark:text-pink-200',
'cashier' => 'bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200',
+ 'customer_portal' => 'bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200',
'customer' => 'bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200',
'viewer' => 'bg-zinc-100 dark:bg-zinc-700 text-zinc-800 dark:text-zinc-200',
default => 'bg-zinc-100 dark:bg-zinc-700 text-zinc-800 dark:text-zinc-200'
diff --git a/app/Models/Customer.php b/app/Models/Customer.php
index 51bc322..6edc982 100644
--- a/app/Models/Customer.php
+++ b/app/Models/Customer.php
@@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Customer extends Model
{
@@ -12,6 +13,7 @@ class Customer extends Model
use HasFactory;
protected $fillable = [
+ 'user_id',
'first_name',
'last_name',
'email',
@@ -30,6 +32,14 @@ class Customer extends Model
'last_service_date' => 'datetime',
];
+ /**
+ * Get the user account associated with this customer
+ */
+ public function user(): BelongsTo
+ {
+ return $this->belongsTo(User::class);
+ }
+
public function vehicles(): HasMany
{
return $this->hasMany(Vehicle::class);
diff --git a/app/Models/User.php b/app/Models/User.php
index a908246..e218f70 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -122,6 +122,37 @@ class User extends Authenticatable
return $this->hasRole('service_advisor');
}
+ /**
+ * Check if user is a customer
+ */
+ public function isCustomer(): bool
+ {
+ return $this->hasRole('customer_portal');
+ }
+
+ /**
+ * Get the customer profile associated with this user (if they are a customer)
+ */
+ public function customer()
+ {
+ return $this->hasOne(Customer::class);
+ }
+
+ /**
+ * Get user's active roles
+ */
+ public function activeRoles()
+ {
+ return $this->roles()
+ ->where('user_roles.is_active', true)
+ ->where(function ($q) {
+ $q->whereNull('user_roles.expires_at')
+ ->orWhere('user_roles.expires_at', '>', now());
+ })
+ ->where('roles.is_active', true)
+ ->get();
+ }
+
/**
* Get the options for logging activities.
*/
diff --git a/app/Models/Vehicle.php b/app/Models/Vehicle.php
index 23377be..28cacd1 100644
--- a/app/Models/Vehicle.php
+++ b/app/Models/Vehicle.php
@@ -48,6 +48,11 @@ class Vehicle extends Model
return $this->hasMany(Appointment::class);
}
+ public function jobCards(): HasMany
+ {
+ return $this->hasMany(JobCard::class);
+ }
+
public function inspections(): HasMany
{
return $this->hasMany(VehicleInspection::class);
diff --git a/app/Settings/InventorySettings.php b/app/Settings/InventorySettings.php
index d63be7d..df980c9 100644
--- a/app/Settings/InventorySettings.php
+++ b/app/Settings/InventorySettings.php
@@ -20,30 +20,30 @@ class InventorySettings extends Settings
public bool $include_labor_in_estimates;
// Additional Pricing Fields (missing)
- public float $default_part_markup;
- public float $core_charge_percentage;
- public float $shop_supply_fee;
- public float $environmental_fee;
- public float $waste_oil_fee;
- public float $tire_disposal_fee;
+ public float $default_part_markup = 0.0;
+ public float $core_charge_percentage = 0.0;
+ public float $shop_supply_fee = 0.0;
+ public float $environmental_fee = 0.0;
+ public float $waste_oil_fee = 0.0;
+ public float $tire_disposal_fee = 0.0;
// Supplier Settings
public int $preferred_supplier_count;
public bool $require_multiple_quotes;
public float $minimum_order_amount;
- public string $default_payment_terms;
- public string $preferred_ordering_method;
- public ?float $free_shipping_threshold;
+ public string $default_payment_terms = '';
+ public string $preferred_ordering_method = '';
+ public ?float $free_shipping_threshold = null;
// Additional Boolean Settings (missing)
- public bool $enable_low_stock_alerts;
- public bool $track_serial_numbers;
- public bool $enable_volume_discounts;
- public bool $enable_seasonal_pricing;
- public bool $enable_customer_specific_pricing;
- public bool $require_po_approval;
- public bool $enable_dropship;
- public bool $enable_backorders;
+ public bool $enable_low_stock_alerts = false;
+ public bool $track_serial_numbers = false;
+ public bool $enable_volume_discounts = false;
+ public bool $enable_seasonal_pricing = false;
+ public bool $enable_customer_specific_pricing = false;
+ public bool $require_po_approval = false;
+ public bool $enable_dropship = false;
+ public bool $enable_backorders = false;
// Part Categories
public array $part_categories;
diff --git a/app/Settings/NotificationSettings.php b/app/Settings/NotificationSettings.php
index 1ccca50..0d9949b 100644
--- a/app/Settings/NotificationSettings.php
+++ b/app/Settings/NotificationSettings.php
@@ -9,7 +9,7 @@ class NotificationSettings extends Settings
// Email Settings
public string $from_email;
public string $from_name;
- public string $manager_email;
+ public string $manager_email = '';
public bool $enable_customer_notifications;
public bool $enable_technician_notifications;
public bool $enable_manager_notifications;
diff --git a/bootstrap/app.php b/bootstrap/app.php
index 5481729..9f7817a 100644
--- a/bootstrap/app.php
+++ b/bootstrap/app.php
@@ -14,6 +14,7 @@ return Application::configure(basePath: dirname(__DIR__))
$middleware->alias([
'role' => \App\Http\Middleware\RoleMiddleware::class,
'permission' => \App\Http\Middleware\PermissionMiddleware::class,
+ 'admin.only' => \App\Http\Middleware\AdminOnly::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
diff --git a/database/migrations/2025_08_01_124842_add_user_id_to_customers_table.php b/database/migrations/2025_08_01_124842_add_user_id_to_customers_table.php
new file mode 100644
index 0000000..9ccec54
--- /dev/null
+++ b/database/migrations/2025_08_01_124842_add_user_id_to_customers_table.php
@@ -0,0 +1,30 @@
+foreignId('user_id')->nullable()->after('id')->constrained()->onDelete('cascade');
+ $table->index('user_id');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('customers', function (Blueprint $table) {
+ $table->dropForeign(['user_id']);
+ $table->dropColumn('user_id');
+ });
+ }
+};
diff --git a/public/robots.txt b/public/robots.txt
deleted file mode 100644
index eb05362..0000000
--- a/public/robots.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-User-agent: *
-Disallow:
diff --git a/resources/views/components/app-layout.blade.php b/resources/views/components/app-layout.blade.php
new file mode 100644
index 0000000..681d507
--- /dev/null
+++ b/resources/views/components/app-layout.blade.php
@@ -0,0 +1,5 @@
+
+
Track your vehicle service progress
++ Track your vehicle's service progress, view estimates, and stay connected with our team throughout the repair process. +
+When you drop off your vehicle, we'll send you an email with a direct link to track your service progress.
+Click the "View Service Status" link in your email to access your personalized dashboard.
+Monitor real-time progress, review estimates, and communicate with our service team.
+See where your vehicle is in our service process with real-time updates.
+View detailed repair estimates and approve or decline work online.
+Receive notifications and communicate directly with our service team.
+If you haven't received your portal access email or need assistance, contact us:
+ +{{ Auth::user()->name }}
+{{ Auth::user()->email }}
+{{ session('message') }}
+Track your vehicle service progress
+{{ $jobCard->customer->name ?? 'Customer' }}
+{{ $jobCard->customer->email ?? '' }}
+#{{ $jobCard->id }}
++ {{ $jobCard->vehicle->year ?? '' }} {{ $jobCard->vehicle->make ?? '' }} {{ $jobCard->vehicle->model ?? '' }} +
+{{ $jobCard->vehicle->license_plate ?? '' }}
+View and manage your service appointments.
+| Date & Time | +Service Type | +Vehicle | +Service Advisor | +Status | +Actions | +
|---|---|---|---|---|---|
|
+
+ {{ $appointment->appointment_datetime->format('M j, Y') }}
+
+
+ {{ $appointment->appointment_datetime->format('g:i A') }}
+
+ |
+
+ {{ ucfirst(str_replace('_', ' ', $appointment->appointment_type)) }}
+ @if($appointment->customer_notes)
+ {{ Str::limit($appointment->customer_notes, 50) }}
+ @endif
+ |
+
+
+ {{ $appointment->vehicle->year ?? '' }}
+ {{ $appointment->vehicle->make ?? '' }}
+ {{ $appointment->vehicle->model ?? '' }}
+
+ {{ $appointment->vehicle->license_plate ?? '' }}
+ |
+ + {{ $appointment->assignedTechnician->name ?? 'Not assigned' }} + | ++ + {{ ucfirst($appointment->status) }} + + | ++ @if($appointment->status === 'pending') + + @endif + | +
+ @if($filterStatus || $filterDate) + Try adjusting your filters or contact us to schedule a new appointment. + @else + You don't have any appointments yet. Contact us to schedule your first service. + @endif +
+Here's an overview of your vehicle services and appointments.
++ {{ $jobCard->vehicle->year ?? '' }} {{ $jobCard->vehicle->make ?? '' }} {{ $jobCard->vehicle->model ?? '' }} +
+ @if($jobCard->serviceAdvisor) +Advisor: {{ $jobCard->serviceAdvisor->name }}
+ @endif +You don't have any vehicles currently being serviced.
++ + {{ $activity['title'] }} + +
+{{ $activity['description'] }}
+{{ $activity['date']->diffForHumans() }}
+Your activity will appear here once you start using our services.
+{{ $appointment->scheduled_datetime->format('M j, Y g:i A') }}
+ @if($appointment->notes) +{{ $appointment->notes }}
+ @endif +{{ session('message') }}
+Created: {{ $estimate->created_at->format('M j, Y g:i A') }}
+ @if($estimate->diagnosis) +Diagnosis: {{ $estimate->diagnosis->summary ?? 'No summary provided' }}
+ @endif +| Description | +Type | +Quantity | +Unit Price | +Total | +
|---|---|---|---|---|
|
+ {{ $item->description }}
+ @if($item->notes)
+ {{ $item->notes }}
+ @endif
+ |
+ + + {{ ucfirst($item->type) }} + + | ++ {{ $item->quantity }} + @if($item->type === 'labor') + {{ $item->quantity == 1 ? 'hour' : 'hours' }} + @endif + | ++ ${{ number_format($item->unit_price, 2) }} + | ++ ${{ number_format($item->total_price, 2) }} + | +
| Subtotal: | +${{ number_format($estimate->subtotal_amount, 2) }} | +|||
| Tax: | +${{ number_format($estimate->tax_amount, 2) }} | +|||
| Total: | +${{ number_format($estimate->total_amount, 2) }} | +|||
No line items found for this estimate.
+{{ $estimate->notes }}
+Please review the estimate above and choose your response:
+ +Your comments: {{ $estimate->notes }}
+Please let us know why you're declining this estimate so we can better assist you:
+ + + +View and manage your service estimates.
+| Estimate # | +Vehicle | +Date | +Amount | +Status | +Actions | +
|---|---|---|---|---|---|
|
+ #{{ $estimate->id }}
+ Job Card #{{ $estimate->job_card_id }}
+ |
+
+
+ {{ $estimate->jobCard->vehicle->year ?? '' }}
+ {{ $estimate->jobCard->vehicle->make ?? '' }}
+ {{ $estimate->jobCard->vehicle->model ?? '' }}
+
+ {{ $estimate->jobCard->vehicle->license_plate ?? '' }}
+ |
+
+ {{ $estimate->created_at->format('M j, Y') }}
+ {{ $estimate->created_at->format('g:i A') }}
+ |
+
+ ${{ number_format($estimate->total_amount, 2) }}
+ @if($estimate->subtotal_amount != $estimate->total_amount)
+
+ Subtotal: ${{ number_format($estimate->subtotal_amount, 2) }}
+ @if($estimate->tax_amount > 0)
+ + Tax: ${{ number_format($estimate->tax_amount, 2) }}
+ @endif
+
+ @endif
+ |
+ + + {{ ucfirst($estimate->status) }} + + | ++ View Details + @if(in_array($estimate->status, ['sent', 'viewed'])) + | + Action Required + @endif + | +
+ @if($filterStatus) + Try adjusting your filters or contact us about your vehicle service. + @else + You don't have any estimates yet. Contact us to schedule a service. + @endif +
+View and manage your service invoices and payment history.
+| Invoice # | +Vehicle | +Date | +Amount | +Status | +Actions | +
|---|---|---|---|---|---|
|
+ #{{ $invoice->order_number }}
+ Service Order
+ |
+
+
+ {{ $invoice->vehicle->year ?? '' }}
+ {{ $invoice->vehicle->make ?? '' }}
+ {{ $invoice->vehicle->model ?? '' }}
+
+ {{ $invoice->vehicle->license_plate ?? '' }}
+ |
+
+ {{ $invoice->completed_at ? \Carbon\Carbon::parse($invoice->completed_at)->format('M j, Y') : 'N/A' }}
+ {{ $invoice->completed_at ? \Carbon\Carbon::parse($invoice->completed_at)->format('g:i A') : '' }}
+ |
+
+ ${{ number_format($invoice->total_amount, 2) }}
+ @if($invoice->labor_cost + $invoice->parts_cost != $invoice->total_amount)
+
+ Labor: ${{ number_format($invoice->labor_cost, 2) }}
+ Parts: ${{ number_format($invoice->parts_cost, 2) }}
+ @if($invoice->tax_amount > 0)
+ + Tax: ${{ number_format($invoice->tax_amount, 2) }}
+ @endif
+
+ @endif
+ |
+ + + {{ ucfirst($invoice->status) }} + + | ++ View Details + @if($invoice->status === 'completed') + | + Download Available + @endif + | +
+ @if($filterStatus) + Try adjusting your filters or contact us about your service history. + @else + You don't have any invoices yet. Complete a service to see invoices here. + @endif +
+You have {{ $invoices->count() }} completed service order(s) totaling ${{ number_format($invoices->sum('total_amount'), 2) }}.
+{{ session('message') }}
+{{ $step['title'] }}
+{{ $step['description'] }}
+Created: {{ $estimate->created_at->format('M j, Y g:i A') }}
+${{ number_format($estimate->total_amount, 2) }}
+{{ $jobCard->vehicle->year }} {{ $jobCard->vehicle->make }} {{ $jobCard->vehicle->model }}
+VIN: {{ $jobCard->vehicle->vin ?? 'Not provided' }}
+License: {{ $jobCard->vehicle->license_plate ?? 'Not provided' }}
+Mileage: {{ number_format($jobCard->vehicle->mileage ?? 0) }} miles
+{{ $jobCard->serviceAdvisor->name ?? 'Not assigned' }}
+ @if($jobCard->serviceAdvisor) +{{ $jobCard->serviceAdvisor->email ?? '' }}
+{{ $jobCard->serviceAdvisor->phone ?? '' }}
+ @endif +{{ $jobCard->description }}
+View information about your registered vehicles.
+{{ Str::limit($vehicle->notes, 100) }}
+Contact us to add your vehicles to your account.
+Track the progress of your vehicle services.
++ {{ $workOrder->vehicle->year ?? '' }} + {{ $workOrder->vehicle->make ?? '' }} + {{ $workOrder->vehicle->model ?? '' }} + @if($workOrder->vehicle->license_plate) + • {{ $workOrder->vehicle->license_plate }} + @endif +
+Created: {{ $workOrder->created_at->format('M j, Y g:i A') }}
+{{ $workOrder->customer_reported_issues }}
+{{ $workOrder->serviceAdvisor->name }}
++ @if($filterStatus) + Try adjusting your filters or contact us about your vehicle service. + @else + You don't have any work orders yet. Contact us to schedule a service. + @endif +
+{{ session('success') }}
-