From cbae4564b9c45402d8c889cba71932d476e1b39d Mon Sep 17 00:00:00 2001 From: sackey Date: Fri, 8 Aug 2025 09:56:26 +0000 Subject: [PATCH] Add customer portal views for dashboard, estimates, invoices, vehicles, and work orders - Implemented dashboard view with vehicle stats, active services, recent activity, and upcoming appointments. - Created estimates view with filtering options and a list of service estimates. - Developed invoices view to manage service invoices and payment history with filtering. - Added vehicles view to display registered vehicles and their details. - Built work orders view to track the progress of vehicle services with filtering and detailed information. --- app/Console/Commands/CheckUserDetails.php | 59 +++ app/Console/Commands/CheckUserRoles.php | 49 ++ app/Console/Commands/CreateCustomerUser.php | 74 +++ app/Console/Commands/CreateTestCustomer.php | 68 +++ app/Console/Commands/LinkCustomersToUsers.php | 41 ++ app/Console/Commands/ResetUserPassword.php | 39 ++ app/Console/Commands/SetupCustomerRoles.php | 45 ++ .../Commands/ShowCustomerUserIntegration.php | 63 +++ app/Console/Commands/TestCustomerCreation.php | 113 +++++ app/Console/Commands/TestUserAuth.php | 52 +++ app/Http/Middleware/AdminOnly.php | 31 ++ app/Livewire/CustomerPortal/Appointments.php | 51 +++ app/Livewire/CustomerPortal/Dashboard.php | 143 ++++++ app/Livewire/CustomerPortal/Estimates.php | 44 ++ app/Livewire/CustomerPortal/Invoices.php | 51 +++ app/Livewire/CustomerPortal/Vehicles.php | 29 ++ app/Livewire/CustomerPortal/WorkOrders.php | 42 ++ app/Livewire/Customers/Create.php | 90 +++- app/Livewire/Customers/Edit.php | 187 +++++--- app/Livewire/Customers/Index.php | 10 +- app/Livewire/Customers/Show.php | 24 +- app/Livewire/Timesheets/Index.php | 201 +++++++-- app/Livewire/Users/Index.php | 23 +- app/Livewire/Users/ManageRolesPermissions.php | 23 +- app/Livewire/Users/Show.php | 3 +- app/Models/Customer.php | 10 + app/Models/User.php | 31 ++ app/Models/Vehicle.php | 5 + app/Settings/InventorySettings.php | 34 +- app/Settings/NotificationSettings.php | 2 +- bootstrap/app.php | 1 + ..._124842_add_user_id_to_customers_table.php | 30 ++ public/robots.txt | 2 - .../views/components/app-layout.blade.php | 5 + resources/views/components/app-logo.blade.php | 6 +- .../layouts/app/sidebar-new.blade.php | 157 ------- .../layouts/app/sidebar-old.blade.php | 335 -------------- .../components/layouts/app/sidebar.blade.php | 20 +- .../customer-portal/appointments.blade.php | 0 .../views/customer-portal/dashboard.blade.php | 0 .../views/customer-portal/index.blade.php | 174 ++++++++ .../views/customer-portal/profile.blade.php | 0 .../layouts/customer-portal-app.blade.php | 121 +++++ .../views/layouts/customer-portal.blade.php | 105 +++++ .../customer-portal/appointments.blade.php | 130 ++++++ .../customer-portal/dashboard.blade.php | 245 ++++++++++ .../customer-portal/estimate-view.blade.php | 224 +++++++++- .../customer-portal/estimates.blade.php | 132 ++++++ .../customer-portal/invoices.blade.php | 145 ++++++ .../customer-portal/job-status.blade.php | 186 +++++++- .../customer-portal/vehicles.blade.php | 92 ++++ .../customer-portal/work-orders.blade.php | 176 ++++++++ .../views/livewire/customers/create.blade.php | 100 ++++- .../views/livewire/customers/edit.blade.php | 124 +++++- .../views/livewire/customers/index.blade.php | 14 + .../views/livewire/customers/show.blade.php | 98 +++- .../views/livewire/timesheets/index.blade.php | 257 ++++++++++- .../views/livewire/users/index.blade.php | 420 ++++++++++-------- .../users/manage-roles-permissions.blade.php | 68 ++- resources/views/livewire/users/show.blade.php | 85 ++++ resources/views/settings/general.blade.php | 10 + routes/test.php | 23 - routes/web.php | 39 +- 63 files changed, 4276 insertions(+), 885 deletions(-) create mode 100644 app/Console/Commands/CheckUserDetails.php create mode 100644 app/Console/Commands/CheckUserRoles.php create mode 100644 app/Console/Commands/CreateCustomerUser.php create mode 100644 app/Console/Commands/CreateTestCustomer.php create mode 100644 app/Console/Commands/LinkCustomersToUsers.php create mode 100644 app/Console/Commands/ResetUserPassword.php create mode 100644 app/Console/Commands/SetupCustomerRoles.php create mode 100644 app/Console/Commands/ShowCustomerUserIntegration.php create mode 100644 app/Console/Commands/TestCustomerCreation.php create mode 100644 app/Console/Commands/TestUserAuth.php create mode 100644 app/Http/Middleware/AdminOnly.php create mode 100644 app/Livewire/CustomerPortal/Appointments.php create mode 100644 app/Livewire/CustomerPortal/Dashboard.php create mode 100644 app/Livewire/CustomerPortal/Estimates.php create mode 100644 app/Livewire/CustomerPortal/Invoices.php create mode 100644 app/Livewire/CustomerPortal/Vehicles.php create mode 100644 app/Livewire/CustomerPortal/WorkOrders.php create mode 100644 database/migrations/2025_08_01_124842_add_user_id_to_customers_table.php delete mode 100644 public/robots.txt create mode 100644 resources/views/components/app-layout.blade.php delete mode 100644 resources/views/components/layouts/app/sidebar-new.blade.php delete mode 100644 resources/views/components/layouts/app/sidebar-old.blade.php rename app/Services/NhtsaVehicleService.php => resources/views/customer-portal/appointments.blade.php (100%) create mode 100644 resources/views/customer-portal/dashboard.blade.php create mode 100644 resources/views/customer-portal/index.blade.php create mode 100644 resources/views/customer-portal/profile.blade.php create mode 100644 resources/views/layouts/customer-portal-app.blade.php create mode 100644 resources/views/layouts/customer-portal.blade.php create mode 100644 resources/views/livewire/customer-portal/appointments.blade.php create mode 100644 resources/views/livewire/customer-portal/dashboard.blade.php create mode 100644 resources/views/livewire/customer-portal/estimates.blade.php create mode 100644 resources/views/livewire/customer-portal/invoices.blade.php create mode 100644 resources/views/livewire/customer-portal/vehicles.blade.php create mode 100644 resources/views/livewire/customer-portal/work-orders.blade.php delete mode 100644 routes/test.php 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 @@ + +
+ {{ $slot }} +
+
diff --git a/resources/views/components/app-logo.blade.php b/resources/views/components/app-logo.blade.php index 706f570..015ebbc 100644 --- a/resources/views/components/app-logo.blade.php +++ b/resources/views/components/app-logo.blade.php @@ -1,6 +1,8 @@
- SafeTrack Systems Logo + {{ app(\App\Settings\GeneralSettings::class)->shop_name ?? 'SafeTrack Systems' }} Logo
- SafeTrack Systems + + {{ app(\App\Settings\GeneralSettings::class)->shop_name ?? 'SafeTrack Systems' }} +
diff --git a/resources/views/components/layouts/app/sidebar-new.blade.php b/resources/views/components/layouts/app/sidebar-new.blade.php deleted file mode 100644 index d7c957a..0000000 --- a/resources/views/components/layouts/app/sidebar-new.blade.php +++ /dev/null @@ -1,157 +0,0 @@ - - - - @include('partials.head') - @fluxAppearance - - - - - - - - - - - - Dashboard - Job Cards - Customers - Work Orders - - - - - - - - - - - - - - Profile - Settings - - - -
- @csrf - Logout -
-
-
-
- - - - - - - - - - - Dashboard - Job Cards - Customers - Vehicles - Appointments - Inspections - Diagnostics - Work Orders - - - Estimates - Invoices - - - - Inventory - Service Items - Technicians - - - - - - - Reports - @can('manage-users') - User Management - @endcan - Settings - - - - - -
- -
- @if(request()->is('job-cards*')) - - All Job Cards - Create New - Received - In Diagnosis - In Progress - Completed - - @elseif(request()->routeIs('customers.*')) - - All Customers - Add Customer - Recent Customers - Customer Reports - - @elseif(request()->is('work-orders*')) - - All Work Orders - Pending - In Progress - Completed - On Hold - - @elseif(request()->is('inventory*')) - - Dashboard - Parts - Low Stock - Purchase Orders - Suppliers - - @elseif(request()->is('reports*')) - - All Reports - Sales Reports - Technician Performance - Parts Usage - Customer Reports - - @else - - Dashboard - New Job Cards - Active Work Orders - Pending Estimates - Inventory Status - Reports - - @endif -
- - - - -
- {{ $slot }} -
-
-
- - @livewireScripts - @fluxScripts - - diff --git a/resources/views/components/layouts/app/sidebar-old.blade.php b/resources/views/components/layouts/app/sidebar-old.blade.php deleted file mode 100644 index eb7f01b..0000000 --- a/resources/views/components/layouts/app/sidebar-old.blade.php +++ /dev/null @@ -1,335 +0,0 @@ - - - - @include('partials.head') - @fluxAppearance - - - - - - - - - - - - Dashboard - Job Cards - Customers - Work Orders - - - - - - - - - - - - - - Profile - Settings - - - -
- @csrf - Logout -
-
-
-
- - - - - - - - - - - - - @include('partials.theme') - - - - - - -
- - - -
- - - - -
-
- - - - - -
- {{ $slot }} -
- - - - @livewireScripts - @fluxScripts - - diff --git a/resources/views/components/layouts/app/sidebar.blade.php b/resources/views/components/layouts/app/sidebar.blade.php index e9eb9a1..40df2b3 100644 --- a/resources/views/components/layouts/app/sidebar.blade.php +++ b/resources/views/components/layouts/app/sidebar.blade.php @@ -11,7 +11,7 @@
- +
@@ -23,26 +23,12 @@
- - -
- @if(auth()->user()->hasPermission('customers.create')) - - @endif - - @if(auth()->user()->hasPermission('job-cards.create')) - - @endif - - @if(auth()->user()->hasPermission('appointments.create')) - - @endif -
+ - + @if(auth()->user()->hasPermission('customers.create')) diff --git a/app/Services/NhtsaVehicleService.php b/resources/views/customer-portal/appointments.blade.php similarity index 100% rename from app/Services/NhtsaVehicleService.php rename to resources/views/customer-portal/appointments.blade.php diff --git a/resources/views/customer-portal/dashboard.blade.php b/resources/views/customer-portal/dashboard.blade.php new file mode 100644 index 0000000..e69de29 diff --git a/resources/views/customer-portal/index.blade.php b/resources/views/customer-portal/index.blade.php new file mode 100644 index 0000000..5bd2259 --- /dev/null +++ b/resources/views/customer-portal/index.blade.php @@ -0,0 +1,174 @@ + + + + + + + + {{ config('app.name', 'AutoRepair Pro') }} - Customer Portal + + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + +
+ +
+
+
+
+ +
+

Customer Portal

+

Track your vehicle service progress

+
+
+
+
+
+ + +
+
+ + + + +

Welcome to Our Customer Portal

+

+ Track your vehicle's service progress, view estimates, and stay connected with our team throughout the repair process. +

+
+ + +
+
+
+

How to Access Your Service Information

+ +
+
+
+
+ 1 +
+
+
+

Check Your Email

+

When you drop off your vehicle, we'll send you an email with a direct link to track your service progress.

+
+
+ +
+
+
+ 2 +
+
+
+

Follow the Link

+

Click the "View Service Status" link in your email to access your personalized dashboard.

+
+
+ +
+
+
+ 3 +
+
+
+

Stay Updated

+

Monitor real-time progress, review estimates, and communicate with our service team.

+
+
+
+
+
+
+ + +
+

What You Can Do

+ +
+
+
+ + + +
+

Track Progress

+

See where your vehicle is in our service process with real-time updates.

+
+ +
+
+ + + +
+

Review Estimates

+

View detailed repair estimates and approve or decline work online.

+
+ +
+
+ + + +
+

Stay Connected

+

Receive notifications and communicate directly with our service team.

+
+
+
+ + +
+
+

Need Help?

+

If you haven't received your portal access email or need assistance, contact us:

+ +
+ @php $generalSettings = app(\App\Settings\GeneralSettings::class); @endphp + + @if($generalSettings->shop_phone) + + + + + {{ $generalSettings->shop_phone }} + + @endif + + @if($generalSettings->shop_email) + + + + + {{ $generalSettings->shop_email }} + + @endif +
+
+
+
+ + +
+
+
+

+ © {{ date('Y') }} {{ $generalSettings->shop_name ?? config('app.name') }}. All rights reserved. +

+
+
+
+
+ + diff --git a/resources/views/customer-portal/profile.blade.php b/resources/views/customer-portal/profile.blade.php new file mode 100644 index 0000000..e69de29 diff --git a/resources/views/layouts/customer-portal-app.blade.php b/resources/views/layouts/customer-portal-app.blade.php new file mode 100644 index 0000000..395cd46 --- /dev/null +++ b/resources/views/layouts/customer-portal-app.blade.php @@ -0,0 +1,121 @@ + + + + + + + + {{ config('app.name', 'AutoRepair Pro') }} - Customer Portal + + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + @livewireStyles + + +
+ +
+
+
+
+ +
+

Customer Portal

+
+
+ +
+
+

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

+

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

+
+
+ {{ substr(Auth::user()->name, 0, 1) }} +
+
+ @csrf + +
+
+
+
+
+ + + + + +
+ + @if (session()->has('message')) +
+
+
+ + + +
+
+

{{ session('message') }}

+
+
+
+ @endif + + + {{ $slot }} +
+ + + +
+ + @livewireScripts + + diff --git a/resources/views/layouts/customer-portal.blade.php b/resources/views/layouts/customer-portal.blade.php new file mode 100644 index 0000000..2f89d30 --- /dev/null +++ b/resources/views/layouts/customer-portal.blade.php @@ -0,0 +1,105 @@ + + + + + + + + {{ config('app.name', 'AutoRepair Pro') }} - Customer Portal + + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + @livewireStyles + + +
+ +
+
+
+
+ +
+

Customer Portal

+

Track your vehicle service progress

+
+
+ +
+
+

{{ $jobCard->customer->name ?? 'Customer' }}

+

{{ $jobCard->customer->email ?? '' }}

+
+
+ {{ substr($jobCard->customer->name ?? 'C', 0, 1) }} +
+
+
+
+
+ + +
+ +
+
+
+

Job Card

+

#{{ $jobCard->id }}

+
+
+

Vehicle

+

+ {{ $jobCard->vehicle->year ?? '' }} {{ $jobCard->vehicle->make ?? '' }} {{ $jobCard->vehicle->model ?? '' }} +

+

{{ $jobCard->vehicle->license_plate ?? '' }}

+
+
+

Status

+ + {{ ucfirst(str_replace('_', ' ', $jobCard->status)) }} + +
+
+
+ + + {{ $slot }} +
+ + + +
+ + @livewireScripts + + diff --git a/resources/views/livewire/customer-portal/appointments.blade.php b/resources/views/livewire/customer-portal/appointments.blade.php new file mode 100644 index 0000000..1bfc01f --- /dev/null +++ b/resources/views/livewire/customer-portal/appointments.blade.php @@ -0,0 +1,130 @@ +
+
+

My Appointments

+

View and manage your service appointments.

+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+ @if($appointments->count() > 0) +
+ + + + + + + + + + + + + @foreach($appointments as $appointment) + + + + + + + + + @endforeach + +
Date & TimeService TypeVehicleService AdvisorStatusActions
+
+ {{ $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($appointments->hasPages()) +
+ {{ $appointments->links() }} +
+ @endif + @else +
+ + + +

No appointments found

+

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

+
+ @endif +
+
diff --git a/resources/views/livewire/customer-portal/dashboard.blade.php b/resources/views/livewire/customer-portal/dashboard.blade.php new file mode 100644 index 0000000..fcf6d6d --- /dev/null +++ b/resources/views/livewire/customer-portal/dashboard.blade.php @@ -0,0 +1,245 @@ +
+ +
+
+
+

Welcome back, {{ Auth::user()->name }}!

+

Here's an overview of your vehicle services and appointments.

+
+ +
+
+ + +
+
+
+
+ + + +
+
+
+
Total Vehicles
+
{{ $stats['total_vehicles'] }}
+
+
+
+
+ +
+
+
+ + + +
+
+
+
Active Services
+
{{ $stats['active_jobs'] }}
+
+
+
+
+ +
+
+
+ + + +
+
+
+
Pending Estimates
+
{{ $stats['pending_estimates'] }}
+
+
+
+
+ +
+
+
+ + + +
+
+
+
Completed Services
+
{{ $stats['completed_services'] }}
+
+
+
+
+
+ +
+ +
+
+

Active Services

+
+
+ @if($activeJobCards->count() > 0) +
+ @foreach($activeJobCards as $jobCard) +
+
+
+

Job Card #{{ $jobCard->id }}

+

+ {{ $jobCard->vehicle->year ?? '' }} {{ $jobCard->vehicle->make ?? '' }} {{ $jobCard->vehicle->model ?? '' }} +

+ @if($jobCard->serviceAdvisor) +

Advisor: {{ $jobCard->serviceAdvisor->name }}

+ @endif +
+
+ + {{ ucfirst(str_replace('_', ' ', $jobCard->status)) }} + + + View Details + +
+
+
+ @endforeach +
+ + @else +
+ + + +

No active services

+

You don't have any vehicles currently being serviced.

+
+ @endif +
+
+ + +
+
+

Recent Activity

+
+
+ @if($recentActivity->count() > 0) +
+ @foreach($recentActivity as $activity) +
+
+ @if($activity['type'] === 'job_card') +
+ + + +
+ @else +
+ + + +
+ @endif +
+
+

+ + {{ $activity['title'] }} + +

+

{{ $activity['description'] }}

+

{{ $activity['date']->diffForHumans() }}

+
+
+ @endforeach +
+ @else +
+ + + +

No recent activity

+

Your activity will appear here once you start using our services.

+
+ @endif +
+
+
+ + + @if($upcomingAppointments->count() > 0) +
+
+

Upcoming Appointments

+
+
+
+ @foreach($upcomingAppointments as $appointment) +
+
+

{{ $appointment->service_type }}

+

{{ $appointment->scheduled_datetime->format('M j, Y g:i A') }}

+ @if($appointment->notes) +

{{ $appointment->notes }}

+ @endif +
+ + {{ ucfirst($appointment->status) }} + +
+ @endforeach +
+ +
+
+ @endif +
diff --git a/resources/views/livewire/customer-portal/estimate-view.blade.php b/resources/views/livewire/customer-portal/estimate-view.blade.php index a40248d..1c4e614 100644 --- a/resources/views/livewire/customer-portal/estimate-view.blade.php +++ b/resources/views/livewire/customer-portal/estimate-view.blade.php @@ -1,3 +1,221 @@ -
- {{-- Knowing others is intelligence; knowing yourself is true wisdom. --}} -
+ +
+ + @if (session()->has('message')) +
+
+
+ + + +
+
+

{{ session('message') }}

+
+
+
+ @endif + + + + + +
+
+
+

Estimate #{{ $estimate->id }}

+

Created: {{ $estimate->created_at->format('M j, Y g:i A') }}

+ @if($estimate->diagnosis) +

Diagnosis: {{ $estimate->diagnosis->summary ?? 'No summary provided' }}

+ @endif +
+
+ + {{ ucfirst($estimate->status) }} + +
+
+
+ + +
+
+

Estimate Details

+
+ + @if($estimate->lineItems && $estimate->lineItems->count() > 0) +
+ + + + + + + + + + + + @foreach($estimate->lineItems as $item) + + + + + + + + @endforeach + + + + + + + @if($estimate->tax_amount > 0) + + + + + @endif + + + + + +
DescriptionTypeQuantityUnit PriceTotal
+
{{ $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) }}
+
+ @else +
+

No line items found for this estimate.

+
+ @endif +
+ + + @if($estimate->notes) +
+

Additional Notes

+

{{ $estimate->notes }}

+
+ @endif + + + @if(in_array($estimate->status, ['sent', 'viewed'])) +
+

Your Response Required

+

Please review the estimate above and choose your response:

+ +
+ + + +
+
+ @endif + + + @if(in_array($estimate->status, ['approved', 'rejected'])) +
+

Your Response

+
+ @if($estimate->status === 'approved') + + + + You approved this estimate on {{ $estimate->customer_approved_at->format('M j, Y g:i A') }} + @else + + + + You declined this estimate on {{ $estimate->customer_approved_at->format('M j, Y g:i A') }} + @endif +
+ @if($estimate->status === 'rejected' && $estimate->notes) +
+

Your comments: {{ $estimate->notes }}

+
+ @endif +
+ @endif +
+ + + +
diff --git a/resources/views/livewire/customer-portal/estimates.blade.php b/resources/views/livewire/customer-portal/estimates.blade.php new file mode 100644 index 0000000..0bf7f70 --- /dev/null +++ b/resources/views/livewire/customer-portal/estimates.blade.php @@ -0,0 +1,132 @@ +
+
+

Estimates

+

View and manage your service estimates.

+
+ + +
+
+
+ + +
+
+ +
+
+
+ + +
+ @if($estimates->count() > 0) +
+ + + + + + + + + + + + + @foreach($estimates as $estimate) + + + + + + + + + @endforeach + +
Estimate #VehicleDateAmountStatusActions
+
#{{ $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($estimates->hasPages()) +
+ {{ $estimates->links() }} +
+ @endif + @else +
+ + + +

No estimates found

+

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

+
+ @endif +
+
diff --git a/resources/views/livewire/customer-portal/invoices.blade.php b/resources/views/livewire/customer-portal/invoices.blade.php new file mode 100644 index 0000000..8a39e27 --- /dev/null +++ b/resources/views/livewire/customer-portal/invoices.blade.php @@ -0,0 +1,145 @@ +
+
+

Invoices

+

View and manage your service invoices and payment history.

+
+ + +
+
+
+ + +
+
+ +
+
+
+ + +
+ @if($invoices->count() > 0) +
+ + + + + + + + + + + + + @foreach($invoices as $invoice) + + + + + + + + + @endforeach + +
Invoice #VehicleDateAmountStatusActions
+
#{{ $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($invoices->hasPages()) +
+ {{ $invoices->links() }} +
+ @endif + @else +
+ + + +

No invoices found

+

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

+
+ @endif +
+ + @if($invoices->count() > 0) +
+
+
+ + + +
+
+

Service History

+
+

You have {{ $invoices->count() }} completed service order(s) totaling ${{ number_format($invoices->sum('total_amount'), 2) }}.

+
+
+
+
+ @endif +
diff --git a/resources/views/livewire/customer-portal/job-status.blade.php b/resources/views/livewire/customer-portal/job-status.blade.php index a573dbb..e184e63 100644 --- a/resources/views/livewire/customer-portal/job-status.blade.php +++ b/resources/views/livewire/customer-portal/job-status.blade.php @@ -1,3 +1,183 @@ -
- {{-- Success is as dangerous as failure. --}} -
+ +
+ + @if (session()->has('message')) +
+
+
+ + + +
+
+

{{ session('message') }}

+
+
+
+ @endif + + +
+
+

Service Progress

+ +
+ +
+
    + @php + $steps = [ + ['key' => 'received', 'title' => 'Vehicle Received', 'description' => 'Your vehicle has been checked in'], + ['key' => 'inspection', 'title' => 'Initial Inspection', 'description' => 'Comprehensive vehicle inspection completed'], + ['key' => 'diagnosis', 'title' => 'Diagnosis', 'description' => 'Issue diagnosis and root cause analysis'], + ['key' => 'estimate', 'title' => 'Estimate Provided', 'description' => 'Repair estimate prepared and sent'], + ['key' => 'approved', 'title' => 'Work Approved', 'description' => 'Estimate approved, work can begin'], + ['key' => 'in_progress', 'title' => 'Work in Progress', 'description' => 'Repairs and services being performed'], + ['key' => 'completed', 'title' => 'Work Completed', 'description' => 'All repairs finished, ready for pickup'], + ]; + $currentStep = $jobCard->status; + @endphp + + @foreach($steps as $index => $step) + @php + $isCompleted = in_array($currentStep, ['inspection', 'diagnosis', 'estimate_sent', 'estimate_approved', 'in_progress', 'completed']) && $step['key'] === 'received' || + in_array($currentStep, ['diagnosis', 'estimate_sent', 'estimate_approved', 'in_progress', 'completed']) && $step['key'] === 'inspection' || + in_array($currentStep, ['estimate_sent', 'estimate_approved', 'in_progress', 'completed']) && $step['key'] === 'diagnosis' || + in_array($currentStep, ['estimate_sent', 'estimate_approved', 'in_progress', 'completed']) && $step['key'] === 'estimate' || + in_array($currentStep, ['estimate_approved', 'in_progress', 'completed']) && $step['key'] === 'approved' || + in_array($currentStep, ['in_progress', 'completed']) && $step['key'] === 'in_progress' || + $currentStep === 'completed' && $step['key'] === 'completed'; + + $isCurrent = ($currentStep === 'pending' && $step['key'] === 'received') || + ($currentStep === 'inspection' && $step['key'] === 'inspection') || + ($currentStep === 'diagnosis' && $step['key'] === 'diagnosis') || + ($currentStep === 'estimate_sent' && $step['key'] === 'estimate') || + ($currentStep === 'estimate_approved' && $step['key'] === 'approved') || + ($currentStep === 'in_progress' && $step['key'] === 'in_progress') || + ($currentStep === 'completed' && $step['key'] === 'completed'); + @endphp + +
  • +
    + @if($index < count($steps) - 1) + + @endif +
    +
    + + @if($isCompleted) + + + + @else + + @endif + +
    +
    +
    +

    {{ $step['title'] }}

    +

    {{ $step['description'] }}

    +
    + @if($isCurrent) +
    + + Current + +
    + @endif +
    +
    +
    +
  • + @endforeach +
+
+
+ + + @if($jobCard->estimates && $jobCard->estimates->count() > 0) +
+

Estimates

+
+ @foreach($jobCard->estimates as $estimate) +
+
+
+

Estimate #{{ $estimate->id }}

+

Created: {{ $estimate->created_at->format('M j, Y g:i A') }}

+

${{ number_format($estimate->total_amount, 2) }}

+
+
+ + {{ ucfirst($estimate->status) }} + + @if(in_array($estimate->status, ['sent', 'viewed'])) + + Review Estimate + + @endif +
+
+
+ @endforeach +
+
+ @endif + + +
+

Vehicle Information

+
+
+

Vehicle Details

+
+

{{ $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

+
+
+
+

Service Advisor

+
+

{{ $jobCard->serviceAdvisor->name ?? 'Not assigned' }}

+ @if($jobCard->serviceAdvisor) +

{{ $jobCard->serviceAdvisor->email ?? '' }}

+

{{ $jobCard->serviceAdvisor->phone ?? '' }}

+ @endif +
+
+
+
+ + + @if($jobCard->description) +
+

Service Description

+

{{ $jobCard->description }}

+
+ @endif +
+
diff --git a/resources/views/livewire/customer-portal/vehicles.blade.php b/resources/views/livewire/customer-portal/vehicles.blade.php new file mode 100644 index 0000000..7c10287 --- /dev/null +++ b/resources/views/livewire/customer-portal/vehicles.blade.php @@ -0,0 +1,92 @@ +
+
+

My Vehicles

+

View information about your registered vehicles.

+
+ + @if($vehicles->count() > 0) +
+ @foreach($vehicles as $vehicle) +
+
+
+
+ + + +
+
+ + Active + +
+
+ +

+ {{ $vehicle->year }} {{ $vehicle->make }} {{ $vehicle->model }} +

+ +
+ @if($vehicle->license_plate) +
+ License Plate: + {{ $vehicle->license_plate }} +
+ @endif + + @if($vehicle->vin) +
+ VIN: + {{ Str::limit($vehicle->vin, 10) }}... +
+ @endif + + @if($vehicle->color) +
+ Color: + {{ ucfirst($vehicle->color) }} +
+ @endif + + @if($vehicle->mileage) +
+ Mileage: + {{ number_format($vehicle->mileage) }} miles +
+ @endif +
+ +
+
+
+
{{ $vehicle->job_cards_count }}
+
Service Records
+
+
+
{{ $vehicle->appointments_count }}
+
Appointments
+
+
+
+ + @if($vehicle->notes) +
+

{{ Str::limit($vehicle->notes, 100) }}

+
+ @endif +
+
+ @endforeach +
+ @else +
+
+ + + +

No vehicles registered

+

Contact us to add your vehicles to your account.

+
+
+ @endif +
diff --git a/resources/views/livewire/customer-portal/work-orders.blade.php b/resources/views/livewire/customer-portal/work-orders.blade.php new file mode 100644 index 0000000..2c79fd5 --- /dev/null +++ b/resources/views/livewire/customer-portal/work-orders.blade.php @@ -0,0 +1,176 @@ +
+
+

Work Orders

+

Track the progress of your vehicle services.

+
+ + +
+
+
+ + +
+
+ +
+
+
+ + +
+ @if($workOrders->count() > 0) + @foreach($workOrders as $workOrder) +
+
+
+
+

Work Order #{{ $workOrder->id }}

+

+ {{ $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') }}

+
+
+ + {{ ucfirst(str_replace('_', ' ', $workOrder->status)) }} + +
+
+ + @if($workOrder->customer_reported_issues) +
+

Reported Issues

+

{{ $workOrder->customer_reported_issues }}

+
+ @endif + + @if($workOrder->serviceAdvisor) +
+

Service Advisor

+

{{ $workOrder->serviceAdvisor->name }}

+
+ @endif + + @if($workOrder->estimates->count() > 0) +
+

Estimates

+
+ @foreach($workOrder->estimates as $estimate) +
+
+ Estimate #{{ $estimate->id }} + ${{ number_format($estimate->total_amount, 2) }} +
+
+ + {{ ucfirst($estimate->status) }} + + @if(in_array($estimate->status, ['sent', 'viewed'])) + + Review + + @endif +
+
+ @endforeach +
+
+ @endif + +
+
+ Last updated: {{ $workOrder->updated_at->diffForHumans() }} +
+ + View Details + +
+
+
+ @endforeach + + @if($workOrders->hasPages()) +
+ {{ $workOrders->links() }} +
+ @endif + @else +
+
+ + + +

No work orders found

+

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

+
+
+ @endif +
+
diff --git a/resources/views/livewire/customers/create.blade.php b/resources/views/livewire/customers/create.blade.php index 3056541..1ef75af 100644 --- a/resources/views/livewire/customers/create.blade.php +++ b/resources/views/livewire/customers/create.blade.php @@ -2,8 +2,61 @@
- Add New Customer - Create a new customer profile + +
+ Additional Information +
+ + Notes + + + +
+
+ + +
+ Customer Portal Access +
+
+ + + Create customer portal account + Allow this customer to login and access their portal + +
+ + @if($create_user_account) +
+
+
+ + Password * +
+ + + Generate + +
+ + Customer will receive this password via email +
+
+ +
+ + + Send welcome email with login details + +
+
+
+ @endif +
+
+ + :heading size="xl">Add New Customer + Create a new customer profile and optional user account
@@ -14,7 +67,7 @@
-
+
Personal Information @@ -124,6 +177,47 @@
+ +
+ Customer Portal Access +
+
+ + + Create customer portal account + Allow this customer to login and access their portal + +
+ + @if($create_user_account) +
+
+
+ + Password * +
+ + + Generate + +
+ + Customer will receive this password via email +
+
+ +
+ + + Send welcome email with login details + +
+
+
+ @endif +
+
+
Cancel diff --git a/resources/views/livewire/customers/edit.blade.php b/resources/views/livewire/customers/edit.blade.php index ae66ee2..42e4f2a 100644 --- a/resources/views/livewire/customers/edit.blade.php +++ b/resources/views/livewire/customers/edit.blade.php @@ -3,29 +3,26 @@
Edit Customer - Update customer information for {{ $customer->full_name }} + Update customer information and manage user account for {{ $customer->full_name }} +
+
+ + + Back to Customer + + @if($has_user_account && $customer->user && $customer->user->isCustomer()) + Has Portal Access + @elseif($has_user_account) + Portal Inactive + @else + No Portal Access + @endif
- - - Back to Customer -
- - @if (session()->has('success')) -
-
- -
-

{{ session('success') }}

-
-
-
- @endif -
- +
Personal Information @@ -142,6 +139,97 @@
+ +
+ Customer Portal Access + + @if($has_user_account) + + @if($customer->user && $customer->user->isCustomer()) +
+
+ + Customer has active portal access +
+ @else +
+
+ + Customer portal access is inactive +
+ @endif + +
+
+ + User Status + + +
+ +
+ + + Reset password + Generate new password for customer portal + +
+
+ + @if($reset_password) +
+ + New Password +
+ + + Generate + +
+ + Customer will need to use this password to login +
+
+ @endif +
+ @else + +
+
+ + Customer has no portal access +
+ +
+ + + Create customer portal account + Allow this customer to login and access their portal + +
+ + @if($create_user_account) +
+ + Password +
+ + + Generate + +
+ + Customer will receive this password to access their portal +
+
+ @endif +
+ @endif +
+
diff --git a/resources/views/livewire/customers/index.blade.php b/resources/views/livewire/customers/index.blade.php index ef27e8e..a3d7289 100644 --- a/resources/views/livewire/customers/index.blade.php +++ b/resources/views/livewire/customers/index.blade.php @@ -79,6 +79,7 @@ Address Vehicles + Portal Access
+ + @if($customer->user && $customer->user->isCustomer()) + + + Active + + @else + + + No Access + + @endif +
@if($customer->last_service_date) diff --git a/resources/views/livewire/customers/show.blade.php b/resources/views/livewire/customers/show.blade.php index dd342e4..4a5760b 100644 --- a/resources/views/livewire/customers/show.blade.php +++ b/resources/views/livewire/customers/show.blade.php @@ -3,7 +3,13 @@
{{ $customer->full_name }} - Customer #{{ $customer->id }} - {{ ucfirst($customer->status) }} + Customer #{{ $customer->id }} - {{ ucfirst($customer->status) }} + @if($customer->user && $customer->user->isCustomer()) + Portal Access + @else + No Portal + @endif +
@@ -21,6 +27,34 @@
+ +
+
+
{{ $stats['total_vehicles'] }}
+
Vehicles
+
+
+
{{ $stats['total_service_orders'] }}
+
Service Orders
+
+
+
{{ $stats['total_appointments'] }}
+
Appointments
+
+
+
{{ $stats['active_job_cards'] }}
+
Active Jobs
+
+
+
{{ $stats['completed_services'] }}
+
Completed
+
+
+
${{ number_format($stats['total_spent'], 2) }}
+
Total Spent
+
+
+
@@ -75,6 +109,68 @@
+ +
+
+ Customer Portal Access +
+
+ @if($customer->user && $customer->user->isCustomer()) +
+ +
+
Customer has portal access
+
Can login and view service history
+
+
+ +
+
+ User ID +
#{{ $customer->user->id }}
+
+
+ Account Status +
+ @if($customer->user->status === 'active') + Active + @else + Inactive + @endif +
+
+
+ Last Login +
+ {{ $customer->user->last_login_at ? $customer->user->last_login_at->format('M j, Y g:i A') : 'Never' }} +
+
+
+ +
+
+ Portal URL: /customer-portal +
+
+ @else +
+ +
+
No portal access
+
Customer cannot login to view their information
+
+
+ +
+ + + Create Portal Account + +
+ @endif +
+
+
diff --git a/resources/views/livewire/timesheets/index.blade.php b/resources/views/livewire/timesheets/index.blade.php index 15e5660..61f19c7 100644 --- a/resources/views/livewire/timesheets/index.blade.php +++ b/resources/views/livewire/timesheets/index.blade.php @@ -1,3 +1,258 @@
- {{-- The whole world belongs to you. --}} + +

+ {{ __('Timesheets') }} +

+
+ +
+
+ +
+
+

Timesheets

+

Track technician work hours and job progress

+
+
+ +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + + + + + + + + + + + + + + @forelse($timesheets as $timesheet) + + + + + + + + + + + @empty + + + + @endforelse + +
+ Technician + + Job Card + + Date + + Start Time + + End Time + + Hours + + Status + + Actions +
+
+
+
+ {{ substr($timesheet->user->name, 0, 1) }} +
+
+
+
+ {{ $timesheet->user->name }} +
+
+
+
+
+ @if($timesheet->job_card_id) + + #{{ $timesheet->job_card_id }} + + @else + - + @endif +
+
+ {{ $timesheet->date->format('M j, Y') }} + + {{ $timesheet->start_time ? $timesheet->start_time->format('g:i A') : '-' }} + + {{ $timesheet->end_time ? $timesheet->end_time->format('g:i A') : '-' }} + + {{ number_format($timesheet->hours_worked, 2) }}h + + + {{ ucfirst($timesheet->status) }} + + +
+ + +
+
+
+ + + +

No timesheets found

+

Get started by creating your first timesheet entry.

+ +
+
+
+ + @if($timesheets->hasPages()) +
+ {{ $timesheets->links() }} +
+ @endif +
+
+
+ + + @if($showCreateModal || $showEditModal) +
+
+
+

+ {{ $showCreateModal ? 'Create Timesheet Entry' : 'Edit Timesheet Entry' }} +

+ + +
+
+ + + @error('form.technician_id') {{ $message }} @enderror +
+ +
+ + + @error('form.job_card_id') {{ $message }} @enderror +
+ +
+ + + @error('form.date') {{ $message }} @enderror +
+ +
+
+ + + @error('form.start_time') {{ $message }} @enderror +
+ +
+ + + @error('form.end_time') {{ $message }} @enderror +
+
+ +
+ + + @error('form.description') {{ $message }} @enderror +
+
+ +
+ + +
+ +
+
+
+ @endif
diff --git a/resources/views/livewire/users/index.blade.php b/resources/views/livewire/users/index.blade.php index 3530de3..b9f87b0 100644 --- a/resources/views/livewire/users/index.blade.php +++ b/resources/views/livewire/users/index.blade.php @@ -1,86 +1,82 @@
- -
+ +
-

User Management

+

Users Management

Manage system users, roles, and permissions

- - -
-
-
- Active: {{ $stats['active'] }} -
-
-
- Inactive: {{ $stats['inactive'] }} -
-
-
- Suspended: {{ $stats['suspended'] }} -
-
-
- Total: {{ $stats['total'] }} -
-
-
- @if($this->getSelectedCount() > 0) - -
- {{ $this->getSelectedCount() }} selected - - -
- @endif - - - + - Add New User + Add User
- + +
+
+
Total Users
+
{{ $stats['total'] }}
+
+
+
Active
+
{{ $stats['active'] }}
+
+
+
Inactive
+
{{ $stats['inactive'] }}
+
+
+
Suspended
+
{{ $stats['suspended'] }}
+
+
+
Customers
+
{{ $stats['customers'] }}
+
+
+
Staff
+
{{ $stats['staff'] }}
+
+
+ +
-
-
+
+ +
+ +
+ +
+ +
+ +
+ + +
+ + +
- - -
+ + +
- - @if($this->hasActiveFilters()) - @endif
- -
-
- - - Showing {{ $users->firstItem() ?? 0 }} to {{ $users->lastItem() ?? 0 }} of {{ $users->total() }} users - + + @if(!empty($selectedUsers)) +
+
+
+ {{ count($selectedUsers) }} user(s) selected +
+
+ + + +
+ @endif
+ +
+
+

+ Users ({{ $users->total() }}) +

+
+ + +
+
+
+ +
- - - - - - - + + + @forelse($users as $user) - - - - - - - - - - @empty - @@ -349,12 +399,12 @@
-
- Name + User @if($sortField === 'name') - @if($sortDirection === 'asc') - - - - @else - - - - @endif + + + @endif
- Email + Contact @if($sortField === 'email') - @if($sortDirection === 'asc') - - - - @else - - - - @endif + + + @endif
Employee ID RolesDepartmentStatusPermissionsActions +
+ Status + @if($sortField === 'status') + + + + @endif +
+
+
+ Created + @if($sortField === 'created_at') + + + + @endif +
+
Actions
- + + -
-
+
+
+
{{ $user->initials() }}
-
-
{{ $user->name }}
- @if($user->position) -
{{ $user->position }}
+
+ +
+ {{ $user->position ?? 'User' }} + @if($user->department) + • {{ $user->department }} + @endif +
+ @if($user->employee_id) +
+ ID: {{ $user->employee_id }} +
@endif
+
{{ $user->email }}
@if($user->phone)
{{ $user->phone }}
@endif -
- {{ $user->employee_id ?: '-' }} - - @if($user->roles->count() > 0) -
- @foreach($user->roles->take(2) as $role) - - {{ $role->display_name }} - - @endforeach - @if($user->roles->count() > 2) - - +{{ $user->roles->count() - 2 }} more - - @endif -
- @else - No roles + @if($user->branch_code) +
Branch: {{ $user->branch_code }}
@endif
- {{ $user->department ? ucfirst(str_replace('_', ' ', $user->department)) : '-' }} + +
+ @foreach($user->activeRoles() as $role) + + {{ $role->display_name }} + + @endforeach + @if($user->customer) + + Customer + + @endif +
+ {{ ucfirst($user->status) }} - - {{ $this->getUserPermissionCount($user) }} - + + {{ $user->created_at->format('M j, Y') }} -
+
+
+ title="View"> - + title="Edit"> - - - - - - @if($user->id !== auth()->id()) @if($user->status === 'active') @elseif($user->status === 'inactive') @endif - + @if($user->status !== 'suspended') @endif @@ -329,18 +367,30 @@
-
- - + +
+ + -

No users found

-

Try adjusting your search or filter criteria.

- @if($this->hasActiveFilters()) - +

No users found

+

+ @if($search || $roleFilter || $statusFilter || $departmentFilter || $branchFilter || $customerFilter) + Try adjusting your filters to find more users. + @else + Get started by creating a new user. + @endif +

+ @if(!$search && !$roleFilter && !$statusFilter && !$departmentFilter && !$branchFilter && !$customerFilter) + @endif
-
- - @if($users->hasPages()) -
- {{ $users->links() }} + + @if($users->hasPages()) +
+ {{ $users->links() }} +
+ @endif
- @endif
diff --git a/resources/views/livewire/users/manage-roles-permissions.blade.php b/resources/views/livewire/users/manage-roles-permissions.blade.php index 41faa7f..0ab1c76 100644 --- a/resources/views/livewire/users/manage-roles-permissions.blade.php +++ b/resources/views/livewire/users/manage-roles-permissions.blade.php @@ -3,15 +3,66 @@

Manage Roles & Permissions

-

Configure access control for {{ $user->name }}

+

+ Configure access control for {{ $user->name }} + @if($user->customer) + + Customer Account + + @endif +

+
+
+ @if($user->customer) + + View Customer + + @endif + + Back to User +
- - Back to User -
+ @if($user->customer) + +
+
+ + + +
+

Customer Account

+

+ This user is linked to customer #{{ $user->customer->id }} ({{ $user->customer->first_name }} {{ $user->customer->last_name }}). + For customer portal access, use the "Customer Portal" preset. +

+
+
+
+ + @unless($user->hasRole('customer_portal')) + +
+
+ + + +
+

Missing Customer Portal Access

+

+ This customer user doesn't have the "Customer Portal" role. Click the preset above to grant customer portal access. +

+
+
+
+ @endunless + @endif +

Role Assignment

@@ -42,6 +93,11 @@ class="px-3 py-1 text-xs font-medium text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20 rounded-md hover:bg-amber-100 dark:hover:bg-amber-900/30 transition-colors"> Parts Clerk Preset +
diff --git a/resources/views/livewire/users/show.blade.php b/resources/views/livewire/users/show.blade.php index ae1a8db..03133ef 100644 --- a/resources/views/livewire/users/show.blade.php +++ b/resources/views/livewire/users/show.blade.php @@ -45,12 +45,23 @@ Branch: {{ $user->branch_code }} @endif + @if($user->customer) + + Customer Account + + @endif
+ @if($user->customer) + + View Customer + + @endif + @if($this->canPerformAction('impersonate')) Impersonate @@ -75,6 +86,80 @@
+ +
+
+

User Management

+
+ +
+
+ + +
+
diff --git a/resources/views/settings/general.blade.php b/resources/views/settings/general.blade.php index 97fc9a8..4e9044f 100644 --- a/resources/views/settings/general.blade.php +++ b/resources/views/settings/general.blade.php @@ -104,6 +104,16 @@
+ + + @if(!empty($settings->shop_logo)) +
+ Shop Logo +
+ @endif + @error('shop_logo') +

{{ $message }}

+ @enderror user(); - - if (!$user) { - return response()->json(['error' => 'Not authenticated']); - } - - return response()->json([ - 'user' => [ - 'name' => $user->name, - 'email' => $user->email, - ], - 'roles' => $user->roles->pluck('name'), - 'permissions' => $user->getAllPermissions()->pluck('name'), - 'has_users_view' => $user->hasPermission('users.view'), - 'has_users_manage' => $user->hasPermission('users.manage'), - 'is_super_admin' => $user->hasRole('super_admin'), - ]); -})->middleware('auth'); diff --git a/routes/web.php b/routes/web.php index 372657e..90f5d18 100644 --- a/routes/web.php +++ b/routes/web.php @@ -7,14 +7,28 @@ use App\Http\Controllers\VehicleController; use App\Http\Controllers\ServiceOrderController; Route::get('/', function () { - return view('welcome'); + if (auth()->check()) { + $user = auth()->user(); + + // Check if user is a customer + if ($user->isCustomer()) { + // Redirect customers to customer portal + return redirect('/customer-portal'); + } else { + // Redirect admin/staff to dashboard + return redirect('/dashboard'); + } + } + + // For guests, redirect to login page instead of showing welcome + return redirect('/login'); })->name('home'); Route::view('dashboard', 'dashboard') - ->middleware(['auth', 'verified']) + ->middleware(['auth', 'verified', 'admin.only']) ->name('dashboard'); -Route::middleware(['auth'])->group(function () { +Route::middleware(['auth', 'admin.only'])->group(function () { // Settings routes Route::redirect('settings', 'settings/general'); Volt::route('settings/profile', 'settings.profile')->name('settings.profile'); @@ -37,7 +51,7 @@ Route::middleware(['auth'])->group(function () { Route::get('/security', [App\Http\Controllers\SettingsController::class, 'security'])->name('security'); Route::put('/security', [App\Http\Controllers\SettingsController::class, 'updateSecurity'])->name('security.update'); }); - // Volt::route('settings/appearance', 'settings.appearance')->name('settings.appearance'); + Volt::route('settings/appearance', 'settings.appearance')->name('settings.appearance'); // Car Repair System Routes Route::resource('customers', CustomerController::class)->middleware('permission:customers.view'); @@ -185,12 +199,6 @@ Route::middleware(['auth'])->group(function () { Route::get('/{timesheet}/edit', \App\Livewire\Timesheets\Edit::class)->name('edit'); }); - // Customer Portal Routes - Route::prefix('customer-portal')->name('customer-portal.')->group(function () { - Route::get('/{jobCard}/estimate/{estimate}', \App\Livewire\CustomerPortal\EstimateView::class)->name('estimate'); - Route::get('/{jobCard}/status', \App\Livewire\CustomerPortal\JobStatus::class)->name('status'); - }); - // Reports Dashboard Route Route::view('reports', 'reports')->middleware(['auth', 'permission:reports.view'])->name('reports.index'); @@ -209,4 +217,15 @@ Route::middleware(['auth'])->group(function () { })->middleware(['auth', 'permission:users.view'])->name('user-management'); }); +// Customer Portal Routes (Only auth middleware, accessible to customers) +Route::prefix('customer-portal')->name('customer-portal.')->middleware(['auth'])->group(function () { + Route::get('/', \App\Livewire\CustomerPortal\Dashboard::class)->name('dashboard'); + Route::get('/appointments', \App\Livewire\CustomerPortal\Appointments::class)->name('appointments'); + Route::get('/vehicles', \App\Livewire\CustomerPortal\Vehicles::class)->name('vehicles'); + Route::get('/work-orders', \App\Livewire\CustomerPortal\WorkOrders::class)->name('work-orders'); + Route::get('/estimates', \App\Livewire\CustomerPortal\Estimates::class)->name('estimates'); + Route::get('/invoices', \App\Livewire\CustomerPortal\Invoices::class)->name('invoices'); + Route::get('/status/{jobCard}', \App\Livewire\CustomerPortal\JobStatus::class)->name('status'); +}); + require __DIR__.'/auth.php';