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.
This commit is contained in:
59
app/Console/Commands/CheckUserDetails.php
Normal file
59
app/Console/Commands/CheckUserDetails.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class CheckUserDetails extends Command
|
||||
{
|
||||
protected $signature = 'check:user {email}';
|
||||
protected $description = 'Check user details by email';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$email = $this->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.");
|
||||
}
|
||||
}
|
||||
49
app/Console/Commands/CheckUserRoles.php
Normal file
49
app/Console/Commands/CheckUserRoles.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CheckUserRoles extends Command
|
||||
{
|
||||
protected $signature = 'check:user-roles {email}';
|
||||
protected $description = 'Check user roles and their status in detail';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$email = $this->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("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
74
app/Console/Commands/CreateCustomerUser.php
Normal file
74
app/Console/Commands/CreateCustomerUser.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\User;
|
||||
use App\Models\Customer;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class CreateCustomerUser extends Command
|
||||
{
|
||||
protected $signature = 'customer:create {email} {name} {password}';
|
||||
protected $description = 'Create a customer user account';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$email = $this->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;
|
||||
}
|
||||
}
|
||||
68
app/Console/Commands/CreateTestCustomer.php
Normal file
68
app/Console/Commands/CreateTestCustomer.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\User;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Role;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CreateTestCustomer extends Command
|
||||
{
|
||||
protected $signature = 'create:test-customer';
|
||||
protected $description = 'Create a test customer with known credentials';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$email = 'test@example.com';
|
||||
$password = 'password123';
|
||||
|
||||
DB::transaction(function () use ($email, $password) {
|
||||
// Delete existing test customer and user
|
||||
Customer::where('email', $email)->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;
|
||||
}
|
||||
}
|
||||
41
app/Console/Commands/LinkCustomersToUsers.php
Normal file
41
app/Console/Commands/LinkCustomersToUsers.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\User;
|
||||
use App\Models\Customer;
|
||||
|
||||
class LinkCustomersToUsers extends Command
|
||||
{
|
||||
protected $signature = 'migrate:link-customers-users';
|
||||
protected $description = 'Link existing customers to their user accounts based on email';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
39
app/Console/Commands/ResetUserPassword.php
Normal file
39
app/Console/Commands/ResetUserPassword.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class ResetUserPassword extends Command
|
||||
{
|
||||
protected $signature = 'reset:password {email} {password}';
|
||||
protected $description = 'Reset user password';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$email = $this->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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
45
app/Console/Commands/SetupCustomerRoles.php
Normal file
45
app/Console/Commands/SetupCustomerRoles.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\User;
|
||||
use App\Models\Role;
|
||||
|
||||
class SetupCustomerRoles extends Command
|
||||
{
|
||||
protected $signature = 'setup:customer-roles';
|
||||
protected $description = 'Assign customer_portal role to customer users';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
// Find the customer_portal role
|
||||
$customerRole = Role::where('name', 'customer_portal')->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;
|
||||
}
|
||||
}
|
||||
63
app/Console/Commands/ShowCustomerUserIntegration.php
Normal file
63
app/Console/Commands/ShowCustomerUserIntegration.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\User;
|
||||
use App\Models\Customer;
|
||||
|
||||
class ShowCustomerUserIntegration extends Command
|
||||
{
|
||||
protected $signature = 'show:customer-user-integration';
|
||||
protected $description = 'Show how customers and users are integrated';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
113
app/Console/Commands/TestCustomerCreation.php
Normal file
113
app/Console/Commands/TestCustomerCreation.php
Normal file
@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\Customer;
|
||||
use App\Models\User;
|
||||
use App\Models\Role;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class TestCustomerCreation extends Command
|
||||
{
|
||||
protected $signature = 'test:customer-creation';
|
||||
protected $description = 'Test customer creation process exactly like the form';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->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());
|
||||
}
|
||||
}
|
||||
}
|
||||
52
app/Console/Commands/TestUserAuth.php
Normal file
52
app/Console/Commands/TestUserAuth.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class TestUserAuth extends Command
|
||||
{
|
||||
protected $signature = 'test:user-auth {email} {password}';
|
||||
protected $description = 'Test user authentication with given credentials';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$email = $this->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;
|
||||
}
|
||||
}
|
||||
31
app/Http/Middleware/AdminOnly.php
Normal file
31
app/Http/Middleware/AdminOnly.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AdminOnly
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$user = auth()->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);
|
||||
}
|
||||
}
|
||||
51
app/Livewire/CustomerPortal/Appointments.php
Normal file
51
app/Livewire/CustomerPortal/Appointments.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\CustomerPortal;
|
||||
|
||||
use App\Models\Appointment;
|
||||
use App\Models\Customer;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class Appointments extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public $filterStatus = '';
|
||||
public $filterDate = '';
|
||||
|
||||
public function updatingFilterStatus()
|
||||
{
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
143
app/Livewire/CustomerPortal/Dashboard.php
Normal file
143
app/Livewire/CustomerPortal/Dashboard.php
Normal file
@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\CustomerPortal;
|
||||
|
||||
use App\Models\JobCard;
|
||||
use App\Models\Appointment;
|
||||
use App\Models\Vehicle;
|
||||
use App\Models\Estimate;
|
||||
use App\Models\ServiceOrder;
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class Dashboard extends Component
|
||||
{
|
||||
public $stats = [];
|
||||
public $recentActivity = [];
|
||||
public $upcomingAppointments = [];
|
||||
public $activeJobCards = [];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
// Get the customer record via the relationship
|
||||
$customer = $user->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');
|
||||
}
|
||||
}
|
||||
44
app/Livewire/CustomerPortal/Estimates.php
Normal file
44
app/Livewire/CustomerPortal/Estimates.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\CustomerPortal;
|
||||
|
||||
use App\Models\Estimate;
|
||||
use App\Models\Customer;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class Estimates extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public $filterStatus = '';
|
||||
|
||||
public function updatingFilterStatus()
|
||||
{
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
51
app/Livewire/CustomerPortal/Invoices.php
Normal file
51
app/Livewire/CustomerPortal/Invoices.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\CustomerPortal;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use App\Models\ServiceOrder;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class Invoices extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public $filterStatus = '';
|
||||
public $customer;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->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();
|
||||
}
|
||||
}
|
||||
29
app/Livewire/CustomerPortal/Vehicles.php
Normal file
29
app/Livewire/CustomerPortal/Vehicles.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\CustomerPortal;
|
||||
|
||||
use App\Models\Vehicle;
|
||||
use App\Models\Customer;
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class Vehicles extends Component
|
||||
{
|
||||
public function render()
|
||||
{
|
||||
$user = Auth::user();
|
||||
$customer = Customer::where('email', $user->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');
|
||||
}
|
||||
}
|
||||
42
app/Livewire/CustomerPortal/WorkOrders.php
Normal file
42
app/Livewire/CustomerPortal/WorkOrders.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\CustomerPortal;
|
||||
|
||||
use App\Models\JobCard;
|
||||
use App\Models\Customer;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class WorkOrders extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public $filterStatus = '';
|
||||
|
||||
public function updatingFilterStatus()
|
||||
{
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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 . '%')
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
};
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('customers', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
5
resources/views/components/app-layout.blade.php
Normal file
5
resources/views/components/app-layout.blade.php
Normal file
@ -0,0 +1,5 @@
|
||||
<x-layouts.app.sidebar :title="$title ?? null">
|
||||
<main class="flex-1">
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</x-layouts.app.sidebar>
|
||||
@ -1,6 +1,8 @@
|
||||
<div class="flex aspect-square size-8 items-center justify-center rounded-md">
|
||||
<img src="{{ asset('images/logo-safe.png') }}" alt="SafeTrack Systems Logo" class="w-full h-full object-contain rounded-md">
|
||||
<img src="{{ app(\App\Settings\GeneralSettings::class)->shop_logo ? asset(app(\App\Settings\GeneralSettings::class)->shop_logo) : asset('images/logo-safe.png') }}" alt="{{ app(\App\Settings\GeneralSettings::class)->shop_name ?? 'SafeTrack Systems' }} Logo" class="w-full h-full object-contain rounded-md">
|
||||
</div>
|
||||
<div class="ms-1 grid flex-1 text-start text-sm">
|
||||
<span class="mb-0.5 truncate leading-tight font-semibold">SafeTrack Systems</span>
|
||||
<span class="mb-0.5 truncate leading-tight font-semibold">
|
||||
{{ app(\App\Settings\GeneralSettings::class)->shop_name ?? 'SafeTrack Systems' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -1,157 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
<head>
|
||||
@include('partials.head')
|
||||
@fluxAppearance
|
||||
</head>
|
||||
<body class="min-h-screen bg-white dark:bg-zinc-800">
|
||||
<!-- Flux Header -->
|
||||
<flux:header container class="bg-zinc-50 dark:bg-zinc-900 border-b border-zinc-200 dark:border-zinc-700">
|
||||
<flux:sidebar.toggle class="lg:hidden" icon="bars-2" />
|
||||
|
||||
<flux:brand href="{{ route('dashboard') }}" name="Auto Repair Shop" class="max-lg:hidden" wire:navigate>
|
||||
<x-app-logo />
|
||||
</flux:brand>
|
||||
|
||||
<flux:navbar class="-mb-px max-lg:hidden">
|
||||
<flux:navbar.item icon="home" href="{{ route('dashboard') }}" :current="request()->routeIs('dashboard')" wire:navigate>Dashboard</flux:navbar.item>
|
||||
<flux:navbar.item icon="clipboard-document-list" href="{{ route('job-cards.index') }}" :current="request()->is('job-cards*')" wire:navigate>Job Cards</flux:navbar.item>
|
||||
<flux:navbar.item icon="users" href="{{ route('customers.list') }}" :current="request()->routeIs('customers.*')" wire:navigate>Customers</flux:navbar.item>
|
||||
<flux:navbar.item icon="wrench-screwdriver" href="/work-orders" :current="request()->is('work-orders*')" wire:navigate>Work Orders</flux:navbar.item>
|
||||
</flux:navbar>
|
||||
|
||||
<flux:spacer />
|
||||
|
||||
<flux:navbar class="me-4">
|
||||
<flux:navbar.item icon="magnifying-glass" href="#" label="Search" />
|
||||
<flux:navbar.item class="max-lg:hidden" icon="cog-6-tooth" href="{{ route('settings.profile') }}" label="Settings" wire:navigate />
|
||||
</flux:navbar>
|
||||
|
||||
<flux:dropdown position="top" align="start">
|
||||
<flux:profile name="{{ auth()->user()->name }}" avatar="" />
|
||||
|
||||
<flux:menu>
|
||||
<flux:menu.item icon="user" href="{{ route('settings.profile') }}" wire:navigate>Profile</flux:menu.item>
|
||||
<flux:menu.item icon="cog-6-tooth" href="{{ route('settings.profile') }}" wire:navigate>Settings</flux:menu.item>
|
||||
|
||||
<flux:menu.separator />
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<flux:menu.item icon="arrow-right-start-on-rectangle" type="submit">Logout</flux:menu.item>
|
||||
</form>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</flux:header>
|
||||
|
||||
<!-- Mobile Sidebar -->
|
||||
<flux:sidebar stashable sticky class="lg:hidden bg-zinc-50 dark:bg-zinc-900 border-r border-zinc-200 dark:border-zinc-700">
|
||||
<flux:sidebar.toggle class="lg:hidden" icon="x-mark" />
|
||||
|
||||
<flux:brand href="{{ route('dashboard') }}" name="Auto Repair Shop" class="px-2" wire:navigate>
|
||||
<x-app-logo />
|
||||
</flux:brand>
|
||||
|
||||
<flux:navlist variant="outline">
|
||||
<flux:navlist.item icon="home" href="{{ route('dashboard') }}" :current="request()->routeIs('dashboard')" wire:navigate>Dashboard</flux:navlist.item>
|
||||
<flux:navlist.item icon="clipboard-document-list" href="{{ route('job-cards.index') }}" :current="request()->is('job-cards*')" wire:navigate>Job Cards</flux:navlist.item>
|
||||
<flux:navlist.item icon="users" href="{{ route('customers.list') }}" :current="request()->routeIs('customers.*')" wire:navigate>Customers</flux:navlist.item>
|
||||
<flux:navlist.item icon="truck" href="/vehicles" :current="request()->is('vehicles*')" wire:navigate>Vehicles</flux:navlist.item>
|
||||
<flux:navlist.item icon="calendar" href="/appointments" :current="request()->is('appointments*')" wire:navigate>Appointments</flux:navlist.item>
|
||||
<flux:navlist.item icon="clipboard-document-check" href="/inspections" :current="request()->is('inspections*')" wire:navigate>Inspections</flux:navlist.item>
|
||||
<flux:navlist.item icon="magnifying-glass-circle" href="/diagnosis" :current="request()->is('diagnosis*')" wire:navigate>Diagnostics</flux:navlist.item>
|
||||
<flux:navlist.item icon="wrench-screwdriver" href="/work-orders" :current="request()->is('work-orders*')" wire:navigate>Work Orders</flux:navlist.item>
|
||||
|
||||
<flux:navlist.group expandable heading="Financial">
|
||||
<flux:navlist.item href="/estimates" :current="request()->is('estimates*')" wire:navigate>Estimates</flux:navlist.item>
|
||||
<flux:navlist.item href="/invoices" :current="request()->is('invoices*')" wire:navigate>Invoices</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
|
||||
<flux:navlist.group expandable heading="Resources">
|
||||
<flux:navlist.item href="{{ route('inventory.dashboard') }}" :current="request()->is('inventory*')" wire:navigate>Inventory</flux:navlist.item>
|
||||
<flux:navlist.item href="/service-items" :current="request()->is('service-items*')" wire:navigate>Service Items</flux:navlist.item>
|
||||
<flux:navlist.item href="/technicians" :current="request()->is('technicians*')" wire:navigate>Technicians</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
</flux:navlist>
|
||||
|
||||
<flux:spacer />
|
||||
|
||||
<flux:navlist variant="outline">
|
||||
<flux:navlist.item icon="chart-bar" href="{{ route('reports.index') }}" :current="request()->is('reports*')" wire:navigate>Reports</flux:navlist.item>
|
||||
@can('manage-users')
|
||||
<flux:navlist.item icon="users" href="{{ route('users.index') }}" :current="request()->routeIs('users.*')" wire:navigate>User Management</flux:navlist.item>
|
||||
@endcan
|
||||
<flux:navlist.item icon="cog-6-tooth" href="{{ route('settings.profile') }}" wire:navigate>Settings</flux:navlist.item>
|
||||
</flux:navlist>
|
||||
</flux:sidebar>
|
||||
|
||||
<!-- Main Content with Secondary Sidebar -->
|
||||
<flux:main container>
|
||||
<div class="flex max-md:flex-col items-start">
|
||||
<!-- Secondary Navigation Sidebar -->
|
||||
<div class="w-full md:w-[220px] pb-4 me-10">
|
||||
@if(request()->is('job-cards*'))
|
||||
<flux:navlist>
|
||||
<flux:navlist.item href="{{ route('job-cards.index') }}" :current="request()->routeIs('job-cards.index')" wire:navigate>All Job Cards</flux:navlist.item>
|
||||
<flux:navlist.item href="{{ route('job-cards.create') }}" wire:navigate>Create New</flux:navlist.item>
|
||||
<flux:navlist.item href="{{ route('job-cards.index', ['status' => 'received']) }}" wire:navigate>Received</flux:navlist.item>
|
||||
<flux:navlist.item href="{{ route('job-cards.index', ['status' => 'in_diagnosis']) }}" wire:navigate>In Diagnosis</flux:navlist.item>
|
||||
<flux:navlist.item href="{{ route('job-cards.index', ['status' => 'in_progress']) }}" wire:navigate>In Progress</flux:navlist.item>
|
||||
<flux:navlist.item href="{{ route('job-cards.index', ['status' => 'completed']) }}" wire:navigate>Completed</flux:navlist.item>
|
||||
</flux:navlist>
|
||||
@elseif(request()->routeIs('customers.*'))
|
||||
<flux:navlist>
|
||||
<flux:navlist.item href="{{ route('customers.list') }}" :current="request()->routeIs('customers.list')" wire:navigate>All Customers</flux:navlist.item>
|
||||
<flux:navlist.item href="{{ route('customers.create') }}" wire:navigate>Add Customer</flux:navlist.item>
|
||||
<flux:navlist.item href="#" wire:navigate>Recent Customers</flux:navlist.item>
|
||||
<flux:navlist.item href="#" wire:navigate>Customer Reports</flux:navlist.item>
|
||||
</flux:navlist>
|
||||
@elseif(request()->is('work-orders*'))
|
||||
<flux:navlist>
|
||||
<flux:navlist.item href="/work-orders" :current="request()->is('work-orders') && !request()->has('status')" wire:navigate>All Work Orders</flux:navlist.item>
|
||||
<flux:navlist.item href="/work-orders?status=pending" wire:navigate>Pending</flux:navlist.item>
|
||||
<flux:navlist.item href="/work-orders?status=in_progress" wire:navigate>In Progress</flux:navlist.item>
|
||||
<flux:navlist.item href="/work-orders?status=completed" wire:navigate>Completed</flux:navlist.item>
|
||||
<flux:navlist.item href="/work-orders?status=on_hold" wire:navigate>On Hold</flux:navlist.item>
|
||||
</flux:navlist>
|
||||
@elseif(request()->is('inventory*'))
|
||||
<flux:navlist>
|
||||
<flux:navlist.item href="{{ route('inventory.dashboard') }}" :current="request()->routeIs('inventory.dashboard')" wire:navigate>Dashboard</flux:navlist.item>
|
||||
<flux:navlist.item href="/inventory/parts" wire:navigate>Parts</flux:navlist.item>
|
||||
<flux:navlist.item href="/inventory/low-stock" wire:navigate>Low Stock</flux:navlist.item>
|
||||
<flux:navlist.item href="/inventory/orders" wire:navigate>Purchase Orders</flux:navlist.item>
|
||||
<flux:navlist.item href="/inventory/suppliers" wire:navigate>Suppliers</flux:navlist.item>
|
||||
</flux:navlist>
|
||||
@elseif(request()->is('reports*'))
|
||||
<flux:navlist>
|
||||
<flux:navlist.item href="{{ route('reports.index') }}" :current="request()->routeIs('reports.index')" wire:navigate>All Reports</flux:navlist.item>
|
||||
<flux:navlist.item href="/reports/sales" wire:navigate>Sales Reports</flux:navlist.item>
|
||||
<flux:navlist.item href="/reports/technician" wire:navigate>Technician Performance</flux:navlist.item>
|
||||
<flux:navlist.item href="/reports/parts" wire:navigate>Parts Usage</flux:navlist.item>
|
||||
<flux:navlist.item href="/reports/customer" wire:navigate>Customer Reports</flux:navlist.item>
|
||||
</flux:navlist>
|
||||
@else
|
||||
<flux:navlist>
|
||||
<flux:navlist.item href="{{ route('dashboard') }}" :current="request()->routeIs('dashboard')" wire:navigate>Dashboard</flux:navlist.item>
|
||||
<flux:navlist.item href="{{ route('job-cards.index') }}" badge="{{ \App\Models\JobCard::where('status', 'received')->count() ?: null }}" wire:navigate>New Job Cards</flux:navlist.item>
|
||||
<flux:navlist.item href="/work-orders?status=in_progress" wire:navigate>Active Work Orders</flux:navlist.item>
|
||||
<flux:navlist.item href="/estimates?status=pending" wire:navigate>Pending Estimates</flux:navlist.item>
|
||||
<flux:navlist.item href="{{ route('inventory.dashboard') }}" wire:navigate>Inventory Status</flux:navlist.item>
|
||||
<flux:navlist.item href="{{ route('reports.index') }}" wire:navigate>Reports</flux:navlist.item>
|
||||
</flux:navlist>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<flux:separator class="md:hidden" />
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="flex-1 max-md:pt-6 self-stretch">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
</flux:main>
|
||||
|
||||
@livewireScripts
|
||||
@fluxScripts
|
||||
</body>
|
||||
</html>
|
||||
@ -1,335 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
<head>
|
||||
@include('partials.head')
|
||||
@fluxAppearance
|
||||
</head>
|
||||
<body class="min-h-screen bg-white dark:bg-zinc-800">
|
||||
<!-- Flux Header -->
|
||||
<flux:header container class="bg-zinc-50 dark:bg-zinc-900 border-b border-zinc-200 dark:border-zinc-700">
|
||||
<flux:sidebar.toggle class="lg:hidden" icon="bars-2" />
|
||||
|
||||
<flux:brand href="{{ route('dashboard') }}" name="Auto Repair Shop" class="max-lg:hidden" wire:navigate>
|
||||
<x-app-logo />
|
||||
</flux:brand>
|
||||
|
||||
<flux:navbar class="-mb-px max-lg:hidden">
|
||||
<flux:navbar.item icon="home" href="{{ route('dashboard') }}" :current="request()->routeIs('dashboard')" wire:navigate>Dashboard</flux:navbar.item>
|
||||
<flux:navbar.item icon="clipboard-document-list" href="{{ route('job-cards.index') }}" :current="request()->is('job-cards*')" wire:navigate>Job Cards</flux:navbar.item>
|
||||
<flux:navbar.item icon="users" href="{{ route('customers.list') }}" :current="request()->routeIs('customers.*')" wire:navigate>Customers</flux:navbar.item>
|
||||
<flux:navbar.item icon="wrench-screwdriver" href="/work-orders" :current="request()->is('work-orders*')" wire:navigate>Work Orders</flux:navbar.item>
|
||||
</flux:navbar>
|
||||
|
||||
<flux:spacer />
|
||||
|
||||
<flux:navbar class="me-4">
|
||||
<flux:navbar.item icon="magnifying-glass" href="#" label="Search" />
|
||||
<flux:navbar.item class="max-lg:hidden" icon="cog-6-tooth" href="{{ route('settings.profile') }}" label="Settings" wire:navigate />
|
||||
</flux:navbar>
|
||||
|
||||
<flux:dropdown position="top" align="start">
|
||||
<flux:profile name="{{ auth()->user()->name }}" avatar="" />
|
||||
|
||||
<flux:menu>
|
||||
<flux:menu.item icon="user" href="{{ route('settings.profile') }}" wire:navigate>Profile</flux:menu.item>
|
||||
<flux:menu.item icon="cog-6-tooth" href="{{ route('settings.profile') }}" wire:navigate>Settings</flux:menu.item>
|
||||
|
||||
<flux:menu.separator />
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<flux:menu.item icon="arrow-right-start-on-rectangle" type="submit">Logout</flux:menu.item>
|
||||
</form>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</flux:header>
|
||||
|
||||
<!-- Mobile Sidebar -->
|
||||
<flux:sidebar stashable sticky class="lg:hidden bg-zinc-50 dark:bg-zinc-900 border-r border-zinc-200 dark:border-zinc-700">
|
||||
<flux:sidebar.toggle class="lg:hidden" icon="x-mark" />
|
||||
|
||||
<flux:brand href="{{ route('dashboard') }}" name="Auto Repair Shop" class="px-2" wire:navigate>
|
||||
<x-app-logo />
|
||||
</flux:brand>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="mt-6 px-4 pb-20">
|
||||
<!-- Main Navigation - No sections, just grouped items -->
|
||||
<div class="space-y-1">
|
||||
<a href="{{ route('dashboard') }}"
|
||||
class="flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors {{ request()->routeIs('dashboard') ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300' : 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800' }}"
|
||||
wire:navigate>
|
||||
<svg class="mr-3 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
|
||||
</svg>
|
||||
{{ __('Dashboard') }}
|
||||
</a>
|
||||
|
||||
<a href="{{ route('customers.list') }}"
|
||||
class="flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors {{ request()->routeIs('customers.*') ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300' : 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800' }}"
|
||||
wire:navigate>
|
||||
<svg class="mr-3 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z"></path>
|
||||
</svg>
|
||||
{{ __('Customers') }}
|
||||
</a>
|
||||
|
||||
<a href="/vehicles"
|
||||
class="flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors {{ request()->is('vehicles*') ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300' : 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800' }}"
|
||||
wire:navigate>
|
||||
<svg class="mr-3 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
|
||||
</svg>
|
||||
{{ __('Vehicles') }}
|
||||
</a>
|
||||
|
||||
<a href="/appointments"
|
||||
class="flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors {{ request()->is('appointments*') ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300' : 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800' }}"
|
||||
wire:navigate>
|
||||
<svg class="mr-3 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
{{ __('Appointments') }}
|
||||
</a>
|
||||
|
||||
<a href="{{ route('job-cards.index') }}"
|
||||
class="flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors {{ request()->is('job-cards*') ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300' : 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800' }}"
|
||||
wire:navigate>
|
||||
<svg class="mr-3 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path>
|
||||
</svg>
|
||||
{{ __('Job Cards') }}
|
||||
</a>
|
||||
|
||||
<a href="/inspections"
|
||||
class="flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors {{ request()->is('inspections*') ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300' : 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800' }}"
|
||||
wire:navigate>
|
||||
<svg class="mr-3 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"></path>
|
||||
</svg>
|
||||
{{ __('Inspections') }}
|
||||
</a>
|
||||
|
||||
<a href="/diagnosis"
|
||||
class="flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors {{ request()->is('diagnosis*') ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300' : 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800' }}"
|
||||
wire:navigate>
|
||||
<svg class="mr-3 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
{{ __('Diagnostics') }}
|
||||
</a>
|
||||
|
||||
<a href="/work-orders"
|
||||
class="flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors {{ request()->is('work-orders*') ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300' : 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800' }}"
|
||||
wire:navigate>
|
||||
<svg class="mr-3 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4"></path>
|
||||
</svg>
|
||||
{{ __('Work Orders') }}
|
||||
</a>
|
||||
<!-- Financial & Resources -->
|
||||
<div class="border-t border-zinc-200 dark:border-zinc-700 pt-4 mt-4">
|
||||
<a href="/estimates"
|
||||
class="flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors {{ request()->is('estimates*') ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300' : 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800' }}"
|
||||
wire:navigate>
|
||||
<svg class="mr-3 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
{{ __('Estimates') }}
|
||||
</a>
|
||||
|
||||
<a href="/invoices"
|
||||
class="flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors {{ request()->is('invoices*') ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300' : 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800' }}"
|
||||
wire:navigate>
|
||||
<svg class="mr-3 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
{{ __('Invoices') }}
|
||||
</a>
|
||||
|
||||
<a href="{{ route('inventory.dashboard') }}"
|
||||
class="flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors {{ request()->is('inventory*') ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300' : 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800' }}"
|
||||
wire:navigate>
|
||||
<svg class="mr-3 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path>
|
||||
</svg>
|
||||
{{ __('Inventory') }}
|
||||
</a>
|
||||
|
||||
<a href="/service-items"
|
||||
class="flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors {{ request()->is('service-items*') ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300' : 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800' }}"
|
||||
wire:navigate>
|
||||
<svg class="mr-3 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
{{ __('Service Items') }}
|
||||
</a>
|
||||
|
||||
<a href="/technicians"
|
||||
class="flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors {{ request()->is('technicians*') ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300' : 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800' }}"
|
||||
wire:navigate>
|
||||
<svg class="mr-3 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
||||
</svg>
|
||||
{{ __('Technicians') }}
|
||||
</a>
|
||||
|
||||
<a href="{{ route('reports.index') }}"
|
||||
class="flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors {{ request()->is('reports*') ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300' : 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800' }}"
|
||||
wire:navigate>
|
||||
<svg class="mr-3 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||
</svg>
|
||||
{{ __('Reports') }}
|
||||
</a>
|
||||
|
||||
<!-- Administration -->
|
||||
@can('manage-users')
|
||||
<div class="border-t border-zinc-200 dark:border-zinc-700 pt-4 mt-4">
|
||||
<a href="{{ route('users.index') }}"
|
||||
class="flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors {{ request()->routeIs('users.*') ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300' : 'text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800' }}"
|
||||
wire:navigate>
|
||||
<svg class="mr-3 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z"></path>
|
||||
</svg>
|
||||
{{ __('User Management') }}
|
||||
</a>
|
||||
</div>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- Theme Switcher -->
|
||||
@include('partials.theme')
|
||||
|
||||
<!-- User Menu (Desktop) -->
|
||||
<div class="absolute bottom-0 left-0 right-0 p-4 border-t border-zinc-200 dark:border-zinc-700 hidden lg:block">
|
||||
<div class="relative">
|
||||
<button class="flex items-center w-full px-3 py-2 text-sm font-medium text-zinc-700 dark:text-zinc-300 rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" onclick="toggleUserMenu()">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-zinc-200 dark:bg-zinc-700 rounded-lg flex items-center justify-center text-zinc-600 dark:text-zinc-300 font-semibold mr-3">
|
||||
{{ auth()->user()->initials() }}
|
||||
</div>
|
||||
<div class="flex-1 text-left">
|
||||
<div class="text-sm font-medium text-zinc-900 dark:text-white">{{ auth()->user()->name }}</div>
|
||||
<div class="text-xs text-zinc-500 dark:text-zinc-400">{{ auth()->user()->email }}</div>
|
||||
</div>
|
||||
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- User Dropdown Menu -->
|
||||
<div id="userMenu" class="absolute bottom-full left-0 right-0 mb-2 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-md shadow-lg py-1 hidden">
|
||||
<a href="{{ route('settings.profile') }}" class="flex items-center px-4 py-2 text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700" wire:navigate>
|
||||
<svg class="mr-3 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
{{ __('Settings') }}
|
||||
</a>
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}" class="w-full">
|
||||
@csrf
|
||||
<button type="submit" class="flex items-center w-full px-4 py-2 text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 text-left">
|
||||
<svg class="mr-3 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path>
|
||||
</svg>
|
||||
{{ __('Log Out') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Header -->
|
||||
<div class="lg:hidden bg-white dark:bg-zinc-900 border-b border-zinc-200 dark:border-zinc-700 px-4 py-3 flex items-center justify-between">
|
||||
<button class="p-2 rounded-md text-zinc-500 hover:text-zinc-600 hover:bg-zinc-100 dark:hover:bg-zinc-800" onclick="toggleSidebar()">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Mobile User Menu -->
|
||||
<div class="relative">
|
||||
<button class="flex items-center p-2 rounded-md text-zinc-500 hover:text-zinc-600 hover:bg-zinc-100 dark:hover:bg-zinc-800" onclick="toggleMobileUserMenu()">
|
||||
<div class="w-8 h-8 bg-zinc-200 dark:bg-zinc-700 rounded-lg flex items-center justify-center text-zinc-600 dark:text-zinc-300 font-semibold">
|
||||
{{ auth()->user()->initials() }}
|
||||
</div>
|
||||
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Mobile User Dropdown -->
|
||||
<div id="mobileUserMenu" class="absolute right-0 top-full mt-2 w-48 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-md shadow-lg py-1 hidden z-50">
|
||||
<div class="px-4 py-2 border-b border-zinc-200 dark:border-zinc-700">
|
||||
<div class="text-sm font-medium text-zinc-900 dark:text-white">{{ auth()->user()->name }}</div>
|
||||
<div class="text-xs text-zinc-500 dark:text-zinc-400">{{ auth()->user()->email }}</div>
|
||||
</div>
|
||||
|
||||
<a href="{{ route('settings.profile') }}" class="flex items-center px-4 py-2 text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700" wire:navigate>
|
||||
<svg class="mr-3 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
{{ __('Settings') }}
|
||||
</a>
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}" class="w-full">
|
||||
@csrf
|
||||
<button type="submit" class="flex items-center w-full px-4 py-2 text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 text-left">
|
||||
<svg class="mr-3 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path>
|
||||
</svg>
|
||||
{{ __('Log Out') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Sidebar Overlay -->
|
||||
<div id="sidebarOverlay" class="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden hidden" onclick="toggleSidebar()"></div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="lg:pl-64">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const overlay = document.getElementById('sidebarOverlay');
|
||||
|
||||
sidebar.classList.toggle('-translate-x-full');
|
||||
overlay.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
function toggleUserMenu() {
|
||||
const menu = document.getElementById('userMenu');
|
||||
menu.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
function toggleMobileUserMenu() {
|
||||
const menu = document.getElementById('mobileUserMenu');
|
||||
menu.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
// Close menus when clicking outside
|
||||
document.addEventListener('click', function(event) {
|
||||
const userMenu = document.getElementById('userMenu');
|
||||
const mobileUserMenu = document.getElementById('mobileUserMenu');
|
||||
|
||||
if (!event.target.closest('.relative')) {
|
||||
userMenu?.classList.add('hidden');
|
||||
mobileUserMenu?.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@livewireScripts
|
||||
@fluxScripts
|
||||
</body>
|
||||
</html>
|
||||
@ -11,7 +11,7 @@
|
||||
<!-- Left Section: Sidebar Toggle + Logo -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<flux:sidebar.toggle class="lg:hidden" icon="bars-2" />
|
||||
<flux:brand href="{{ route('dashboard') }}" name="Auto Repair Shop" wire:navigate class="flex items-center">
|
||||
<flux:brand href="{{ route('dashboard') }}" name="" wire:navigate class="flex items-center">
|
||||
<x-app-logo />
|
||||
</flux:brand>
|
||||
</div>
|
||||
@ -23,26 +23,12 @@
|
||||
<div class="flex-1">
|
||||
<livewire:global-search />
|
||||
</div>
|
||||
|
||||
<!-- Quick Action Buttons -->
|
||||
<div class="flex items-center space-x-2">
|
||||
@if(auth()->user()->hasPermission('customers.create'))
|
||||
<flux:button variant="ghost" size="sm" icon="user-plus" href="{{ route('customers.create') }}" wire:navigate title="New Customer" />
|
||||
@endif
|
||||
|
||||
@if(auth()->user()->hasPermission('job-cards.create'))
|
||||
<flux:button variant="ghost" size="sm" icon="clipboard-document-list" href="{{ route('job-cards.create') }}" wire:navigate title="New Job Card" />
|
||||
@endif
|
||||
|
||||
@if(auth()->user()->hasPermission('appointments.create'))
|
||||
<flux:button variant="ghost" size="sm" icon="calendar" href="{{ route('appointments.create') }}" wire:navigate title="New Appointment" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Plus Icon for More Quick Actions -->
|
||||
<flux:dropdown align="end">
|
||||
<flux:button variant="primary" square icon="plus" aria-label="More quick actions" />
|
||||
|
||||
|
||||
<flux:menu>
|
||||
@if(auth()->user()->hasPermission('customers.create'))
|
||||
<flux:menu.item icon="user-plus" href="{{ route('customers.create') }}" wire:navigate>
|
||||
|
||||
0
resources/views/customer-portal/dashboard.blade.php
Normal file
0
resources/views/customer-portal/dashboard.blade.php
Normal file
174
resources/views/customer-portal/index.blade.php
Normal file
174
resources/views/customer-portal/index.blade.php
Normal file
@ -0,0 +1,174 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ config('app.name', 'AutoRepair Pro') }} - Customer Portal</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Scripts -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="font-sans antialiased h-full bg-gray-50">
|
||||
<div class="min-h-full">
|
||||
<!-- Header -->
|
||||
<header class="bg-white shadow-sm border-b border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center py-6">
|
||||
<div class="flex items-center">
|
||||
<x-app-logo class="h-8 w-auto" />
|
||||
<div class="ml-4">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Customer Portal</h1>
|
||||
<p class="text-sm text-gray-600">Track your vehicle service progress</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="max-w-4xl mx-auto py-16 px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center">
|
||||
<svg class="mx-auto h-20 w-20 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 11.172V5l-1-1z"></path>
|
||||
</svg>
|
||||
|
||||
<h2 class="mt-6 text-3xl font-bold text-gray-900">Welcome to Our Customer Portal</h2>
|
||||
<p class="mt-4 text-lg text-gray-600 max-w-2xl mx-auto">
|
||||
Track your vehicle's service progress, view estimates, and stay connected with our team throughout the repair process.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Access Instructions -->
|
||||
<div class="mt-16">
|
||||
<div class="bg-white rounded-lg shadow-lg overflow-hidden">
|
||||
<div class="px-6 py-8">
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-6">How to Access Your Service Information</h3>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 bg-blue-100 rounded-full">
|
||||
<span class="text-blue-600 font-semibold">1</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h4 class="text-lg font-medium text-gray-900">Check Your Email</h4>
|
||||
<p class="text-gray-600">When you drop off your vehicle, we'll send you an email with a direct link to track your service progress.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 bg-blue-100 rounded-full">
|
||||
<span class="text-blue-600 font-semibold">2</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h4 class="text-lg font-medium text-gray-900">Follow the Link</h4>
|
||||
<p class="text-gray-600">Click the "View Service Status" link in your email to access your personalized dashboard.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 bg-blue-100 rounded-full">
|
||||
<span class="text-blue-600 font-semibold">3</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h4 class="text-lg font-medium text-gray-900">Stay Updated</h4>
|
||||
<p class="text-gray-600">Monitor real-time progress, review estimates, and communicate with our service team.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features -->
|
||||
<div class="mt-16">
|
||||
<h3 class="text-xl font-semibold text-gray-900 text-center mb-8">What You Can Do</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div class="text-center">
|
||||
<div class="mx-auto h-12 w-12 bg-green-100 rounded-lg flex items-center justify-center">
|
||||
<svg class="h-6 w-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 class="mt-4 text-lg font-medium text-gray-900">Track Progress</h4>
|
||||
<p class="mt-2 text-gray-600">See where your vehicle is in our service process with real-time updates.</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="mx-auto h-12 w-12 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<svg class="h-6 w-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 class="mt-4 text-lg font-medium text-gray-900">Review Estimates</h4>
|
||||
<p class="mt-2 text-gray-600">View detailed repair estimates and approve or decline work online.</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="mx-auto h-12 w-12 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||
<svg class="h-6 w-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 class="mt-4 text-lg font-medium text-gray-900">Stay Connected</h4>
|
||||
<p class="mt-2 text-gray-600">Receive notifications and communicate directly with our service team.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Information -->
|
||||
<div class="mt-16 bg-gray-100 rounded-lg p-8">
|
||||
<div class="text-center">
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-4">Need Help?</h3>
|
||||
<p class="text-gray-600 mb-6">If you haven't received your portal access email or need assistance, contact us:</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row justify-center items-center space-y-2 sm:space-y-0 sm:space-x-8">
|
||||
@php $generalSettings = app(\App\Settings\GeneralSettings::class); @endphp
|
||||
|
||||
@if($generalSettings->shop_phone)
|
||||
<a href="tel:{{ $generalSettings->shop_phone }}" class="flex items-center text-blue-600 hover:text-blue-800">
|
||||
<svg class="h-5 w-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
|
||||
</svg>
|
||||
{{ $generalSettings->shop_phone }}
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if($generalSettings->shop_email)
|
||||
<a href="mailto:{{ $generalSettings->shop_email }}" class="flex items-center text-blue-600 hover:text-blue-800">
|
||||
<svg class="h-5 w-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
{{ $generalSettings->shop_email }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-white border-t border-gray-200 mt-16">
|
||||
<div class="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-gray-600">
|
||||
© {{ date('Y') }} {{ $generalSettings->shop_name ?? config('app.name') }}. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
0
resources/views/customer-portal/profile.blade.php
Normal file
0
resources/views/customer-portal/profile.blade.php
Normal file
121
resources/views/layouts/customer-portal-app.blade.php
Normal file
121
resources/views/layouts/customer-portal-app.blade.php
Normal file
@ -0,0 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ config('app.name', 'AutoRepair Pro') }} - Customer Portal</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Scripts -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
@livewireStyles
|
||||
</head>
|
||||
<body class="font-sans antialiased h-full bg-gray-50">
|
||||
<div class="min-h-full">
|
||||
<!-- Header -->
|
||||
<header class="bg-white shadow-sm border-b border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center py-4">
|
||||
<div class="flex items-center">
|
||||
<x-app-logo class="h-8 w-auto" />
|
||||
<div class="ml-4">
|
||||
<h1 class="text-xl font-bold text-gray-900">Customer Portal</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="text-right">
|
||||
<p class="text-sm font-medium text-gray-900">{{ Auth::user()->name }}</p>
|
||||
<p class="text-xs text-gray-600">{{ Auth::user()->email }}</p>
|
||||
</div>
|
||||
<div class="h-8 w-8 rounded-full bg-blue-500 flex items-center justify-center text-white font-medium">
|
||||
{{ substr(Auth::user()->name, 0, 1) }}
|
||||
</div>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button type="submit" class="text-sm text-gray-600 hover:text-gray-900">
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex space-x-8">
|
||||
<a href="{{ route('customer-portal.dashboard') }}"
|
||||
class="border-b-2 {{ request()->routeIs('customer-portal.dashboard') ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }} py-4 px-1 text-sm font-medium">
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="{{ route('customer-portal.appointments') }}"
|
||||
class="border-b-2 {{ request()->routeIs('customer-portal.appointments') ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }} py-4 px-1 text-sm font-medium">
|
||||
Appointments
|
||||
</a>
|
||||
<a href="{{ route('customer-portal.vehicles') }}"
|
||||
class="border-b-2 {{ request()->routeIs('customer-portal.vehicles') ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }} py-4 px-1 text-sm font-medium">
|
||||
My Vehicles
|
||||
</a>
|
||||
<a href="{{ route('customer-portal.work-orders') }}"
|
||||
class="border-b-2 {{ request()->routeIs('customer-portal.work-orders') ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }} py-4 px-1 text-sm font-medium">
|
||||
Work Orders
|
||||
</a>
|
||||
<a href="{{ route('customer-portal.estimates') }}"
|
||||
class="border-b-2 {{ request()->routeIs('customer-portal.estimates') ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }} py-4 px-1 text-sm font-medium">
|
||||
Estimates
|
||||
</a>
|
||||
<a href="{{ route('customer-portal.invoices') }}"
|
||||
class="border-b-2 {{ request()->routeIs('customer-portal.invoices') ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }} py-4 px-1 text-sm font-medium">
|
||||
Invoices
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
|
||||
<!-- Flash Messages -->
|
||||
@if (session()->has('message'))
|
||||
<div class="mb-6 rounded-md bg-green-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-green-800">{{ session('message') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Page Content -->
|
||||
{{ $slot }}
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-white border-t border-gray-200 mt-16">
|
||||
<div class="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-gray-600">
|
||||
Questions about your service? Contact us at
|
||||
<a href="tel:{{ app(\App\Settings\GeneralSettings::class)->shop_phone ?? '' }}" class="text-blue-600 hover:text-blue-800">
|
||||
{{ app(\App\Settings\GeneralSettings::class)->shop_phone ?? 'Contact Shop' }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
105
resources/views/layouts/customer-portal.blade.php
Normal file
105
resources/views/layouts/customer-portal.blade.php
Normal file
@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ config('app.name', 'AutoRepair Pro') }} - Customer Portal</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Scripts -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
@livewireStyles
|
||||
</head>
|
||||
<body class="font-sans antialiased h-full bg-gray-50">
|
||||
<div class="min-h-full">
|
||||
<!-- Header -->
|
||||
<header class="bg-white shadow-sm border-b border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center py-6">
|
||||
<div class="flex items-center">
|
||||
<x-app-logo class="h-8 w-auto" />
|
||||
<div class="ml-4">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Customer Portal</h1>
|
||||
<p class="text-sm text-gray-600">Track your vehicle service progress</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="text-right">
|
||||
<p class="text-sm font-medium text-gray-900">{{ $jobCard->customer->name ?? 'Customer' }}</p>
|
||||
<p class="text-xs text-gray-600">{{ $jobCard->customer->email ?? '' }}</p>
|
||||
</div>
|
||||
<div class="h-8 w-8 rounded-full bg-blue-500 flex items-center justify-center text-white font-medium">
|
||||
{{ substr($jobCard->customer->name ?? 'C', 0, 1) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
|
||||
<!-- Job Card Info Bar -->
|
||||
<div class="mb-8 bg-white rounded-lg shadow p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-500">Job Card</h3>
|
||||
<p class="mt-1 text-lg font-semibold text-gray-900">#{{ $jobCard->id }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-500">Vehicle</h3>
|
||||
<p class="mt-1 text-lg font-semibold text-gray-900">
|
||||
{{ $jobCard->vehicle->year ?? '' }} {{ $jobCard->vehicle->make ?? '' }} {{ $jobCard->vehicle->model ?? '' }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">{{ $jobCard->vehicle->license_plate ?? '' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-500">Status</h3>
|
||||
<span class="inline-flex mt-1 px-2 py-1 text-xs font-medium rounded-full
|
||||
@switch($jobCard->status)
|
||||
@case('pending')
|
||||
bg-yellow-100 text-yellow-800
|
||||
@break
|
||||
@case('in_progress')
|
||||
bg-blue-100 text-blue-800
|
||||
@break
|
||||
@case('completed')
|
||||
bg-green-100 text-green-800
|
||||
@break
|
||||
@default
|
||||
bg-gray-100 text-gray-800
|
||||
@endswitch
|
||||
">
|
||||
{{ ucfirst(str_replace('_', ' ', $jobCard->status)) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page Content -->
|
||||
{{ $slot }}
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-white border-t border-gray-200 mt-16">
|
||||
<div class="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-gray-600">
|
||||
Questions about your service? Contact us at
|
||||
<a href="tel:{{ app(\App\Settings\GeneralSettings::class)->shop_phone ?? '' }}" class="text-blue-600 hover:text-blue-800">
|
||||
{{ app(\App\Settings\GeneralSettings::class)->shop_phone ?? 'Contact Shop' }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
130
resources/views/livewire/customer-portal/appointments.blade.php
Normal file
130
resources/views/livewire/customer-portal/appointments.blade.php
Normal file
@ -0,0 +1,130 @@
|
||||
<div>
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900">My Appointments</h1>
|
||||
<p class="text-gray-600">View and manage your service appointments.</p>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Status</label>
|
||||
<select wire:model.live="filterStatus" class="w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="confirmed">Confirmed</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Date</label>
|
||||
<input type="date" wire:model.live="filterDate" class="w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<button wire:click="$set('filterStatus', ''); $set('filterDate', '')"
|
||||
class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">
|
||||
Clear Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Appointments List -->
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
@if($appointments->count() > 0)
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date & Time</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Service Type</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Vehicle</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Service Advisor</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach($appointments as $appointment)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
{{ $appointment->appointment_datetime->format('M j, Y') }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ $appointment->appointment_datetime->format('g:i A') }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">{{ ucfirst(str_replace('_', ' ', $appointment->appointment_type)) }}</div>
|
||||
@if($appointment->customer_notes)
|
||||
<div class="text-sm text-gray-500">{{ Str::limit($appointment->customer_notes, 50) }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">
|
||||
{{ $appointment->vehicle->year ?? '' }}
|
||||
{{ $appointment->vehicle->make ?? '' }}
|
||||
{{ $appointment->vehicle->model ?? '' }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">{{ $appointment->vehicle->license_plate ?? '' }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{{ $appointment->assignedTechnician->name ?? 'Not assigned' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||
@switch($appointment->status)
|
||||
@case('confirmed')
|
||||
bg-green-100 text-green-800
|
||||
@break
|
||||
@case('pending')
|
||||
bg-yellow-100 text-yellow-800
|
||||
@break
|
||||
@case('completed')
|
||||
bg-blue-100 text-blue-800
|
||||
@break
|
||||
@case('cancelled')
|
||||
bg-red-100 text-red-800
|
||||
@break
|
||||
@default
|
||||
bg-gray-100 text-gray-800
|
||||
@endswitch
|
||||
">
|
||||
{{ ucfirst($appointment->status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
@if($appointment->status === 'pending')
|
||||
<button class="text-red-600 hover:text-red-900">Cancel</button>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if($appointments->hasPages())
|
||||
<div class="px-6 py-3 border-t border-gray-200">
|
||||
{{ $appointments->links() }}
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="text-center py-12">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">No appointments found</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
@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
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
245
resources/views/livewire/customer-portal/dashboard.blade.php
Normal file
245
resources/views/livewire/customer-portal/dashboard.blade.php
Normal file
@ -0,0 +1,245 @@
|
||||
<div>
|
||||
<!-- Dashboard Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Welcome back, {{ Auth::user()->name }}!</h1>
|
||||
<p class="text-gray-600">Here's an overview of your vehicle services and appointments.</p>
|
||||
</div>
|
||||
<button wire:click="refreshDashboard"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<svg class="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-8 w-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 11.172V5l-1-1z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Total Vehicles</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ $stats['total_vehicles'] }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-8 w-8 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Active Services</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ $stats['active_jobs'] }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-8 w-8 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Pending Estimates</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ $stats['pending_estimates'] }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-8 w-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Completed Services</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ $stats['completed_services'] }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Active Job Cards -->
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Active Services</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
@if($activeJobCards->count() > 0)
|
||||
<div class="space-y-4">
|
||||
@foreach($activeJobCards as $jobCard)
|
||||
<div class="border rounded-lg p-4 hover:bg-gray-50">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 class="font-medium text-gray-900">Job Card #{{ $jobCard->id }}</h3>
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ $jobCard->vehicle->year ?? '' }} {{ $jobCard->vehicle->make ?? '' }} {{ $jobCard->vehicle->model ?? '' }}
|
||||
</p>
|
||||
@if($jobCard->serviceAdvisor)
|
||||
<p class="text-xs text-gray-500">Advisor: {{ $jobCard->serviceAdvisor->name }}</p>
|
||||
@endif
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||
@switch($jobCard->status)
|
||||
@case('pending')
|
||||
bg-yellow-100 text-yellow-800
|
||||
@break
|
||||
@case('in_progress')
|
||||
bg-blue-100 text-blue-800
|
||||
@break
|
||||
@case('estimate_sent')
|
||||
bg-orange-100 text-orange-800
|
||||
@break
|
||||
@default
|
||||
bg-gray-100 text-gray-800
|
||||
@endswitch
|
||||
">
|
||||
{{ ucfirst(str_replace('_', ' ', $jobCard->status)) }}
|
||||
</span>
|
||||
<a href="{{ route('customer-portal.status', $jobCard) }}"
|
||||
class="block mt-2 text-sm text-blue-600 hover:text-blue-800">
|
||||
View Details
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="mt-4 text-center">
|
||||
<a href="{{ route('customer-portal.work-orders') }}"
|
||||
class="text-blue-600 hover:text-blue-800 text-sm font-medium">
|
||||
View All Work Orders →
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-8">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 11.172V5l-1-1z"></path>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">No active services</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">You don't have any vehicles currently being serviced.</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Recent Activity</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
@if($recentActivity->count() > 0)
|
||||
<div class="space-y-4">
|
||||
@foreach($recentActivity as $activity)
|
||||
<div class="flex space-x-3">
|
||||
<div class="flex-shrink-0">
|
||||
@if($activity['type'] === 'job_card')
|
||||
<div class="h-8 w-8 rounded-full bg-blue-100 flex items-center justify-center">
|
||||
<svg class="h-4 w-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 11.172V5l-1-1z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
@else
|
||||
<div class="h-8 w-8 rounded-full bg-green-100 flex items-center justify-center">
|
||||
<svg class="h-4 w-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900">
|
||||
<a href="{{ $activity['url'] }}" class="hover:underline">
|
||||
{{ $activity['title'] }}
|
||||
</a>
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">{{ $activity['description'] }}</p>
|
||||
<p class="text-xs text-gray-400">{{ $activity['date']->diffForHumans() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-8">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">No recent activity</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Your activity will appear here once you start using our services.</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upcoming Appointments -->
|
||||
@if($upcomingAppointments->count() > 0)
|
||||
<div class="mt-8 bg-white rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Upcoming Appointments</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="space-y-4">
|
||||
@foreach($upcomingAppointments as $appointment)
|
||||
<div class="flex justify-between items-center border-l-4 border-blue-400 pl-4">
|
||||
<div>
|
||||
<h3 class="font-medium text-gray-900">{{ $appointment->service_type }}</h3>
|
||||
<p class="text-sm text-gray-600">{{ $appointment->scheduled_datetime->format('M j, Y g:i A') }}</p>
|
||||
@if($appointment->notes)
|
||||
<p class="text-sm text-gray-500">{{ $appointment->notes }}</p>
|
||||
@endif
|
||||
</div>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||
@switch($appointment->status)
|
||||
@case('confirmed')
|
||||
bg-green-100 text-green-800
|
||||
@break
|
||||
@case('pending')
|
||||
bg-yellow-100 text-yellow-800
|
||||
@break
|
||||
@default
|
||||
bg-gray-100 text-gray-800
|
||||
@endswitch
|
||||
">
|
||||
{{ ucfirst($appointment->status) }}
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="mt-4 text-center">
|
||||
<a href="{{ route('customer-portal.appointments') }}"
|
||||
class="text-blue-600 hover:text-blue-800 text-sm font-medium">
|
||||
View All Appointments →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@ -1,3 +1,221 @@
|
||||
<div>
|
||||
{{-- Knowing others is intelligence; knowing yourself is true wisdom. --}}
|
||||
</div>
|
||||
<x-layouts.customer-portal>
|
||||
<div class="space-y-8">
|
||||
<!-- Flash Messages -->
|
||||
@if (session()->has('message'))
|
||||
<div class="rounded-md bg-green-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-green-800">{{ session('message') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Back to Status -->
|
||||
<div>
|
||||
<a href="{{ route('customer-portal.status', $jobCard) }}" class="inline-flex items-center text-blue-600 hover:text-blue-800">
|
||||
<svg class="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
||||
</svg>
|
||||
Back to Job Status
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Estimate Header -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Estimate #{{ $estimate->id }}</h1>
|
||||
<p class="text-sm text-gray-600 mt-1">Created: {{ $estimate->created_at->format('M j, Y g:i A') }}</p>
|
||||
@if($estimate->diagnosis)
|
||||
<p class="text-sm text-gray-600">Diagnosis: {{ $estimate->diagnosis->summary ?? 'No summary provided' }}</p>
|
||||
@endif
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium
|
||||
@switch($estimate->status)
|
||||
@case('approved')
|
||||
bg-green-100 text-green-800
|
||||
@break
|
||||
@case('rejected')
|
||||
bg-red-100 text-red-800
|
||||
@break
|
||||
@case('sent')
|
||||
@case('viewed')
|
||||
bg-yellow-100 text-yellow-800
|
||||
@break
|
||||
@default
|
||||
bg-gray-100 text-gray-800
|
||||
@endswitch
|
||||
">
|
||||
{{ ucfirst($estimate->status) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estimate Details -->
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Estimate Details</h2>
|
||||
</div>
|
||||
|
||||
@if($estimate->lineItems && $estimate->lineItems->count() > 0)
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Quantity</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Unit Price</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach($estimate->lineItems as $item)
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">{{ $item->description }}</div>
|
||||
@if($item->notes)
|
||||
<div class="text-sm text-gray-500">{{ $item->notes }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||
{{ $item->type === 'labor' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800' }}">
|
||||
{{ ucfirst($item->type) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{{ $item->quantity }}
|
||||
@if($item->type === 'labor')
|
||||
{{ $item->quantity == 1 ? 'hour' : 'hours' }}
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
${{ number_format($item->unit_price, 2) }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
${{ number_format($item->total_price, 2) }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
<tfoot class="bg-gray-50">
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-4 text-right text-sm font-medium text-gray-900">Subtotal:</td>
|
||||
<td class="px-6 py-4 text-sm font-medium text-gray-900">${{ number_format($estimate->subtotal_amount, 2) }}</td>
|
||||
</tr>
|
||||
@if($estimate->tax_amount > 0)
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-4 text-right text-sm font-medium text-gray-900">Tax:</td>
|
||||
<td class="px-6 py-4 text-sm font-medium text-gray-900">${{ number_format($estimate->tax_amount, 2) }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-4 text-right text-lg font-bold text-gray-900">Total:</td>
|
||||
<td class="px-6 py-4 text-lg font-bold text-gray-900">${{ number_format($estimate->total_amount, 2) }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="px-6 py-8 text-center">
|
||||
<p class="text-gray-500">No line items found for this estimate.</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Estimate Notes -->
|
||||
@if($estimate->notes)
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Additional Notes</h2>
|
||||
<p class="text-gray-700 whitespace-pre-line">{{ $estimate->notes }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Customer Actions -->
|
||||
@if(in_array($estimate->status, ['sent', 'viewed']))
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Your Response Required</h2>
|
||||
<p class="text-gray-600 mb-6">Please review the estimate above and choose your response:</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
<button wire:click="approveEstimate"
|
||||
wire:confirm="Are you sure you want to approve this estimate? This will authorize us to begin work on your vehicle."
|
||||
class="inline-flex justify-center items-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
|
||||
<svg class="h-5 w-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
Approve Estimate
|
||||
</button>
|
||||
|
||||
<button onclick="document.getElementById('reject-modal').classList.remove('hidden')"
|
||||
class="inline-flex justify-center items-center px-6 py-3 border border-gray-300 text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<svg class="h-5 w-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
Decline Estimate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Already Responded -->
|
||||
@if(in_array($estimate->status, ['approved', 'rejected']))
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Your Response</h2>
|
||||
<div class="flex items-center">
|
||||
@if($estimate->status === 'approved')
|
||||
<svg class="h-5 w-5 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
<span class="text-green-800 font-medium">You approved this estimate on {{ $estimate->customer_approved_at->format('M j, Y g:i A') }}</span>
|
||||
@else
|
||||
<svg class="h-5 w-5 text-red-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
<span class="text-red-800 font-medium">You declined this estimate on {{ $estimate->customer_approved_at->format('M j, Y g:i A') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@if($estimate->status === 'rejected' && $estimate->notes)
|
||||
<div class="mt-4 p-4 bg-gray-50 rounded-md">
|
||||
<p class="text-sm text-gray-700"><strong>Your comments:</strong> {{ $estimate->notes }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Rejection Modal -->
|
||||
<div id="reject-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
||||
<div class="mt-3">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Decline Estimate</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Please let us know why you're declining this estimate so we can better assist you:</p>
|
||||
|
||||
<textarea wire:model="customerComments"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows="4"
|
||||
placeholder="Your comments or concerns..."></textarea>
|
||||
|
||||
<div class="flex justify-end space-x-3 mt-6">
|
||||
<button onclick="document.getElementById('reject-modal').classList.add('hidden')"
|
||||
class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">
|
||||
Cancel
|
||||
</button>
|
||||
<button wire:click="rejectEstimate"
|
||||
class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700">
|
||||
Decline Estimate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-layouts.customer-portal>
|
||||
|
||||
132
resources/views/livewire/customer-portal/estimates.blade.php
Normal file
132
resources/views/livewire/customer-portal/estimates.blade.php
Normal file
@ -0,0 +1,132 @@
|
||||
<div>
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Estimates</h1>
|
||||
<p class="text-gray-600">View and manage your service estimates.</p>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Status</label>
|
||||
<select wire:model.live="filterStatus" class="w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="draft">Draft</option>
|
||||
<option value="sent">Sent</option>
|
||||
<option value="viewed">Viewed</option>
|
||||
<option value="approved">Approved</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<button wire:click="$set('filterStatus', '')"
|
||||
class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">
|
||||
Clear Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estimates List -->
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
@if($estimates->count() > 0)
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Estimate #</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Vehicle</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Amount</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach($estimates as $estimate)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">#{{ $estimate->id }}</div>
|
||||
<div class="text-sm text-gray-500">Job Card #{{ $estimate->job_card_id }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">
|
||||
{{ $estimate->jobCard->vehicle->year ?? '' }}
|
||||
{{ $estimate->jobCard->vehicle->make ?? '' }}
|
||||
{{ $estimate->jobCard->vehicle->model ?? '' }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">{{ $estimate->jobCard->vehicle->license_plate ?? '' }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">{{ $estimate->created_at->format('M j, Y') }}</div>
|
||||
<div class="text-sm text-gray-500">{{ $estimate->created_at->format('g:i A') }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">${{ number_format($estimate->total_amount, 2) }}</div>
|
||||
@if($estimate->subtotal_amount != $estimate->total_amount)
|
||||
<div class="text-sm text-gray-500">
|
||||
Subtotal: ${{ number_format($estimate->subtotal_amount, 2) }}
|
||||
@if($estimate->tax_amount > 0)
|
||||
+ Tax: ${{ number_format($estimate->tax_amount, 2) }}
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||
@switch($estimate->status)
|
||||
@case('approved')
|
||||
bg-green-100 text-green-800
|
||||
@break
|
||||
@case('rejected')
|
||||
bg-red-100 text-red-800
|
||||
@break
|
||||
@case('sent')
|
||||
@case('viewed')
|
||||
bg-yellow-100 text-yellow-800
|
||||
@break
|
||||
@case('draft')
|
||||
bg-gray-100 text-gray-800
|
||||
@break
|
||||
@default
|
||||
bg-gray-100 text-gray-800
|
||||
@endswitch
|
||||
">
|
||||
{{ ucfirst($estimate->status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<span class="text-blue-600">View Details</span>
|
||||
@if(in_array($estimate->status, ['sent', 'viewed']))
|
||||
<span class="text-gray-300 mx-2">|</span>
|
||||
<span class="text-orange-600 font-medium">Action Required</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if($estimates->hasPages())
|
||||
<div class="px-6 py-3 border-t border-gray-200">
|
||||
{{ $estimates->links() }}
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="text-center py-12">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">No estimates found</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
@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
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
145
resources/views/livewire/customer-portal/invoices.blade.php
Normal file
145
resources/views/livewire/customer-portal/invoices.blade.php
Normal file
@ -0,0 +1,145 @@
|
||||
<div>
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Invoices</h1>
|
||||
<p class="text-gray-600">View and manage your service invoices and payment history.</p>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Status</label>
|
||||
<select wire:model.live="filterStatus" class="w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<button wire:click="$set('filterStatus', '')"
|
||||
class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">
|
||||
Clear Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invoices List -->
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
@if($invoices->count() > 0)
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Invoice #</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Vehicle</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Amount</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach($invoices as $invoice)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">#{{ $invoice->order_number }}</div>
|
||||
<div class="text-sm text-gray-500">Service Order</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">
|
||||
{{ $invoice->vehicle->year ?? '' }}
|
||||
{{ $invoice->vehicle->make ?? '' }}
|
||||
{{ $invoice->vehicle->model ?? '' }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">{{ $invoice->vehicle->license_plate ?? '' }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">{{ $invoice->completed_at ? \Carbon\Carbon::parse($invoice->completed_at)->format('M j, Y') : 'N/A' }}</div>
|
||||
<div class="text-sm text-gray-500">{{ $invoice->completed_at ? \Carbon\Carbon::parse($invoice->completed_at)->format('g:i A') : '' }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">${{ number_format($invoice->total_amount, 2) }}</div>
|
||||
@if($invoice->labor_cost + $invoice->parts_cost != $invoice->total_amount)
|
||||
<div class="text-sm text-gray-500">
|
||||
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
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||
@switch($invoice->status)
|
||||
@case('completed')
|
||||
bg-green-100 text-green-800
|
||||
@break
|
||||
@case('cancelled')
|
||||
bg-red-100 text-red-800
|
||||
@break
|
||||
@case('pending')
|
||||
bg-yellow-100 text-yellow-800
|
||||
@break
|
||||
@default
|
||||
bg-gray-100 text-gray-800
|
||||
@endswitch
|
||||
">
|
||||
{{ ucfirst($invoice->status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<span class="text-blue-600">View Details</span>
|
||||
@if($invoice->status === 'completed')
|
||||
<span class="text-gray-300 mx-2">|</span>
|
||||
<span class="text-purple-600">Download Available</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if($invoices->hasPages())
|
||||
<div class="px-6 py-3 border-t border-gray-200">
|
||||
{{ $invoices->links() }}
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="text-center py-12">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">No invoices found</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
@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
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($invoices->count() > 0)
|
||||
<div class="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-blue-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-blue-800">Service History</h3>
|
||||
<div class="mt-2 text-sm text-blue-700">
|
||||
<p>You have {{ $invoices->count() }} completed service order(s) totaling ${{ number_format($invoices->sum('total_amount'), 2) }}.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@ -1,3 +1,183 @@
|
||||
<div>
|
||||
{{-- Success is as dangerous as failure. --}}
|
||||
</div>
|
||||
<x-layouts.customer-portal>
|
||||
<div class="space-y-8">
|
||||
<!-- Flash Messages -->
|
||||
@if (session()->has('message'))
|
||||
<div class="rounded-md bg-green-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-green-800">{{ session('message') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Progress Timeline -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900">Service Progress</h2>
|
||||
<button wire:click="refreshStatus" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<svg class="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flow-root">
|
||||
<ul class="-mb-8">
|
||||
@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
|
||||
|
||||
<li>
|
||||
<div class="relative pb-8">
|
||||
@if($index < count($steps) - 1)
|
||||
<span class="absolute top-4 left-4 -ml-px h-full w-0.5 {{ $isCompleted ? 'bg-green-600' : 'bg-gray-200' }}" aria-hidden="true"></span>
|
||||
@endif
|
||||
<div class="relative flex space-x-3">
|
||||
<div>
|
||||
<span class="h-8 w-8 rounded-full flex items-center justify-center ring-8 ring-white
|
||||
{{ $isCompleted ? 'bg-green-600' : ($isCurrent ? 'bg-blue-600' : 'bg-gray-400') }}">
|
||||
@if($isCompleted)
|
||||
<svg class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
@else
|
||||
<span class="h-2.5 w-2.5 bg-white rounded-full"></span>
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pt-1.5 flex justify-between space-x-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">{{ $step['title'] }}</p>
|
||||
<p class="text-sm text-gray-500">{{ $step['description'] }}</p>
|
||||
</div>
|
||||
@if($isCurrent)
|
||||
<div class="text-right text-sm">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
Current
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estimates Section -->
|
||||
@if($jobCard->estimates && $jobCard->estimates->count() > 0)
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-6">Estimates</h2>
|
||||
<div class="space-y-4">
|
||||
@foreach($jobCard->estimates as $estimate)
|
||||
<div class="border rounded-lg p-4 {{ $estimate->status === 'approved' ? 'border-green-200 bg-green-50' : 'border-gray-200' }}">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900">Estimate #{{ $estimate->id }}</h3>
|
||||
<p class="text-sm text-gray-600">Created: {{ $estimate->created_at->format('M j, Y g:i A') }}</p>
|
||||
<p class="text-lg font-semibold text-gray-900 mt-2">${{ number_format($estimate->total_amount, 2) }}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||
@switch($estimate->status)
|
||||
@case('approved')
|
||||
bg-green-100 text-green-800
|
||||
@break
|
||||
@case('rejected')
|
||||
bg-red-100 text-red-800
|
||||
@break
|
||||
@case('sent')
|
||||
bg-yellow-100 text-yellow-800
|
||||
@break
|
||||
@default
|
||||
bg-gray-100 text-gray-800
|
||||
@endswitch
|
||||
">
|
||||
{{ ucfirst($estimate->status) }}
|
||||
</span>
|
||||
@if(in_array($estimate->status, ['sent', 'viewed']))
|
||||
<a href="{{ route('customer-portal.estimate', [$jobCard, $estimate]) }}"
|
||||
class="mt-2 inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Review Estimate
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Vehicle Information -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-6">Vehicle Information</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-500">Vehicle Details</h3>
|
||||
<div class="mt-2 space-y-2">
|
||||
<p class="text-sm text-gray-900">{{ $jobCard->vehicle->year }} {{ $jobCard->vehicle->make }} {{ $jobCard->vehicle->model }}</p>
|
||||
<p class="text-sm text-gray-600">VIN: {{ $jobCard->vehicle->vin ?? 'Not provided' }}</p>
|
||||
<p class="text-sm text-gray-600">License: {{ $jobCard->vehicle->license_plate ?? 'Not provided' }}</p>
|
||||
<p class="text-sm text-gray-600">Mileage: {{ number_format($jobCard->vehicle->mileage ?? 0) }} miles</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-500">Service Advisor</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-900">{{ $jobCard->serviceAdvisor->name ?? 'Not assigned' }}</p>
|
||||
@if($jobCard->serviceAdvisor)
|
||||
<p class="text-sm text-gray-600">{{ $jobCard->serviceAdvisor->email ?? '' }}</p>
|
||||
<p class="text-sm text-gray-600">{{ $jobCard->serviceAdvisor->phone ?? '' }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Service Description -->
|
||||
@if($jobCard->description)
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-4">Service Description</h2>
|
||||
<p class="text-gray-700">{{ $jobCard->description }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-layouts.customer-portal>
|
||||
|
||||
92
resources/views/livewire/customer-portal/vehicles.blade.php
Normal file
92
resources/views/livewire/customer-portal/vehicles.blade.php
Normal file
@ -0,0 +1,92 @@
|
||||
<div>
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900">My Vehicles</h1>
|
||||
<p class="text-gray-600">View information about your registered vehicles.</p>
|
||||
</div>
|
||||
|
||||
@if($vehicles->count() > 0)
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@foreach($vehicles as $vehicle)
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="h-12 w-12 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<svg class="h-6 w-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 11.172V5l-1-1z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
Active
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">
|
||||
{{ $vehicle->year }} {{ $vehicle->make }} {{ $vehicle->model }}
|
||||
</h3>
|
||||
|
||||
<div class="space-y-2 text-sm text-gray-600">
|
||||
@if($vehicle->license_plate)
|
||||
<div class="flex justify-between">
|
||||
<span>License Plate:</span>
|
||||
<span class="font-medium">{{ $vehicle->license_plate }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($vehicle->vin)
|
||||
<div class="flex justify-between">
|
||||
<span>VIN:</span>
|
||||
<span class="font-medium font-mono text-xs">{{ Str::limit($vehicle->vin, 10) }}...</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($vehicle->color)
|
||||
<div class="flex justify-between">
|
||||
<span>Color:</span>
|
||||
<span class="font-medium">{{ ucfirst($vehicle->color) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($vehicle->mileage)
|
||||
<div class="flex justify-between">
|
||||
<span>Mileage:</span>
|
||||
<span class="font-medium">{{ number_format($vehicle->mileage) }} miles</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-6 pt-4 border-t border-gray-200">
|
||||
<div class="flex justify-between text-sm">
|
||||
<div class="text-center">
|
||||
<div class="font-semibold text-gray-900">{{ $vehicle->job_cards_count }}</div>
|
||||
<div class="text-gray-500">Service Records</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="font-semibold text-gray-900">{{ $vehicle->appointments_count }}</div>
|
||||
<div class="text-gray-500">Appointments</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($vehicle->notes)
|
||||
<div class="mt-4 p-3 bg-gray-50 rounded-md">
|
||||
<p class="text-sm text-gray-700">{{ Str::limit($vehicle->notes, 100) }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="text-center py-12">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 11.172V5l-1-1z"></path>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">No vehicles registered</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Contact us to add your vehicles to your account.</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
176
resources/views/livewire/customer-portal/work-orders.blade.php
Normal file
176
resources/views/livewire/customer-portal/work-orders.blade.php
Normal file
@ -0,0 +1,176 @@
|
||||
<div>
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Work Orders</h1>
|
||||
<p class="text-gray-600">Track the progress of your vehicle services.</p>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Status</label>
|
||||
<select wire:model.live="filterStatus" class="w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="inspection">Inspection</option>
|
||||
<option value="diagnosis">Diagnosis</option>
|
||||
<option value="estimate_sent">Estimate Sent</option>
|
||||
<option value="estimate_approved">Approved</option>
|
||||
<option value="in_progress">In Progress</option>
|
||||
<option value="completed">Completed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<button wire:click="$set('filterStatus', '')"
|
||||
class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">
|
||||
Clear Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Work Orders List -->
|
||||
<div class="space-y-6">
|
||||
@if($workOrders->count() > 0)
|
||||
@foreach($workOrders as $workOrder)
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900">Work Order #{{ $workOrder->id }}</h3>
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ $workOrder->vehicle->year ?? '' }}
|
||||
{{ $workOrder->vehicle->make ?? '' }}
|
||||
{{ $workOrder->vehicle->model ?? '' }}
|
||||
@if($workOrder->vehicle->license_plate)
|
||||
• {{ $workOrder->vehicle->license_plate }}
|
||||
@endif
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">Created: {{ $workOrder->created_at->format('M j, Y g:i A') }}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium
|
||||
@switch($workOrder->status)
|
||||
@case('pending')
|
||||
bg-yellow-100 text-yellow-800
|
||||
@break
|
||||
@case('inspection')
|
||||
bg-blue-100 text-blue-800
|
||||
@break
|
||||
@case('diagnosis')
|
||||
bg-purple-100 text-purple-800
|
||||
@break
|
||||
@case('estimate_sent')
|
||||
bg-orange-100 text-orange-800
|
||||
@break
|
||||
@case('estimate_approved')
|
||||
bg-green-100 text-green-800
|
||||
@break
|
||||
@case('in_progress')
|
||||
bg-blue-100 text-blue-800
|
||||
@break
|
||||
@case('completed')
|
||||
bg-green-100 text-green-800
|
||||
@break
|
||||
@default
|
||||
bg-gray-100 text-gray-800
|
||||
@endswitch
|
||||
">
|
||||
{{ ucfirst(str_replace('_', ' ', $workOrder->status)) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($workOrder->customer_reported_issues)
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-1">Reported Issues</h4>
|
||||
<p class="text-sm text-gray-600">{{ $workOrder->customer_reported_issues }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($workOrder->serviceAdvisor)
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-1">Service Advisor</h4>
|
||||
<p class="text-sm text-gray-600">{{ $workOrder->serviceAdvisor->name }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($workOrder->estimates->count() > 0)
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-2">Estimates</h4>
|
||||
<div class="space-y-2">
|
||||
@foreach($workOrder->estimates as $estimate)
|
||||
<div class="flex justify-between items-center p-3 bg-gray-50 rounded-md">
|
||||
<div>
|
||||
<span class="text-sm font-medium">Estimate #{{ $estimate->id }}</span>
|
||||
<span class="ml-2 text-sm text-gray-600">${{ number_format($estimate->total_amount, 2) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium
|
||||
@switch($estimate->status)
|
||||
@case('approved')
|
||||
bg-green-100 text-green-800
|
||||
@break
|
||||
@case('rejected')
|
||||
bg-red-100 text-red-800
|
||||
@break
|
||||
@case('sent')
|
||||
@case('viewed')
|
||||
bg-yellow-100 text-yellow-800
|
||||
@break
|
||||
@default
|
||||
bg-gray-100 text-gray-800
|
||||
@endswitch
|
||||
">
|
||||
{{ ucfirst($estimate->status) }}
|
||||
</span>
|
||||
@if(in_array($estimate->status, ['sent', 'viewed']))
|
||||
<a href="{{ route('customer-portal.estimate', [$workOrder, $estimate]) }}"
|
||||
class="text-blue-600 hover:text-blue-800 text-xs">
|
||||
Review
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex justify-between items-center pt-4 border-t border-gray-200">
|
||||
<div class="text-sm text-gray-500">
|
||||
Last updated: {{ $workOrder->updated_at->diffForHumans() }}
|
||||
</div>
|
||||
<a href="{{ route('customer-portal.status', $workOrder) }}"
|
||||
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
View Details
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@if($workOrders->hasPages())
|
||||
<div class="mt-6">
|
||||
{{ $workOrders->links() }}
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="text-center py-12">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">No work orders found</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
@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
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@ -2,8 +2,61 @@
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="xl">Add New Customer</flux:heading>
|
||||
<flux:subheading>Create a new customer profile</flux:subheading>
|
||||
<!-- Additional Information -->
|
||||
<div>
|
||||
<flux:heading size="lg" class="mb-4">Additional Information</flux:heading>
|
||||
<div>
|
||||
<flux:field>
|
||||
<flux:label>Notes</flux:label>
|
||||
<flux:textarea wire:model="notes" placeholder="Enter any additional notes about this customer..." rows="3" />
|
||||
<flux:error name="notes" />
|
||||
</flux:field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Account Settings -->
|
||||
<div>
|
||||
<flux:heading size="lg" class="mb-4">Customer Portal Access</flux:heading>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<flux:field>
|
||||
<flux:checkbox wire:model.live="create_user_account" />
|
||||
<flux:label>Create customer portal account</flux:label>
|
||||
<flux:description>Allow this customer to login and access their portal</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
@if($create_user_account)
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<flux:field>
|
||||
<flux:label>Password *</flux:label>
|
||||
<div class="flex space-x-2">
|
||||
<flux:input wire:model="password" type="text" placeholder="Generated password" class="flex-1" />
|
||||
<flux:button type="button" wire:click="generatePassword" variant="outline" size="sm">
|
||||
Generate
|
||||
</flux:button>
|
||||
</div>
|
||||
<flux:error name="password" />
|
||||
<flux:description>Customer will receive this password via email</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<flux:field>
|
||||
<flux:checkbox wire:model="send_welcome_email" />
|
||||
<flux:label>Send welcome email with login details</flux:label>
|
||||
</flux:field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->:heading size="xl">Add New Customer</flux:heading>
|
||||
<flux:subheading>Create a new customer profile and optional user account</flux:subheading>
|
||||
</div>
|
||||
<flux:button href="/customers-list" variant="outline" size="sm">
|
||||
<flux:icon name="arrow-left" class="size-4" />
|
||||
@ -14,7 +67,7 @@
|
||||
<!-- Customer Form -->
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
||||
<div class="p-6">
|
||||
<form wire:submit="save" class="space-y-6">
|
||||
<form wire:submit="save" class="space-y-8">
|
||||
<!-- Personal Information -->
|
||||
<div>
|
||||
<flux:heading size="lg" class="mb-4">Personal Information</flux:heading>
|
||||
@ -124,6 +177,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Account Settings -->
|
||||
<div>
|
||||
<flux:heading size="lg" class="mb-4">Customer Portal Access</flux:heading>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<flux:field>
|
||||
<flux:checkbox wire:model.live="create_user_account" />
|
||||
<flux:label>Create customer portal account</flux:label>
|
||||
<flux:description>Allow this customer to login and access their portal</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
@if($create_user_account)
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<flux:field>
|
||||
<flux:label>Password *</flux:label>
|
||||
<div class="flex space-x-2">
|
||||
<flux:input wire:model="password" type="text" placeholder="Generated password" class="flex-1" />
|
||||
<flux:button type="button" wire:click="generatePassword" variant="outline" size="sm">
|
||||
Generate
|
||||
</flux:button>
|
||||
</div>
|
||||
<flux:error name="password" />
|
||||
<flux:description>Customer will receive this password via email</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<flux:field>
|
||||
<flux:checkbox wire:model="send_welcome_email" />
|
||||
<flux:label>Send welcome email with login details</flux:label>
|
||||
</flux:field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="flex items-center justify-end space-x-3 pt-6 border-t border-zinc-200 dark:border-zinc-700">
|
||||
<flux:button href="/customers-list" variant="outline">Cancel</flux:button>
|
||||
|
||||
@ -3,29 +3,26 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="xl">Edit Customer</flux:heading>
|
||||
<flux:subheading>Update customer information for {{ $customer->full_name }}</flux:subheading>
|
||||
<flux:subheading>Update customer information and manage user account for {{ $customer->full_name }}</flux:subheading>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<flux:button href="/customers/{{ $customer->id }}" variant="outline" size="sm">
|
||||
<flux:icon name="arrow-left" class="size-4" />
|
||||
Back to Customer
|
||||
</flux:button>
|
||||
@if($has_user_account && $customer->user && $customer->user->isCustomer())
|
||||
<flux:badge variant="success" size="sm">Has Portal Access</flux:badge>
|
||||
@elseif($has_user_account)
|
||||
<flux:badge variant="warning" size="sm">Portal Inactive</flux:badge>
|
||||
@else
|
||||
<flux:badge variant="outline" size="sm">No Portal Access</flux:badge>
|
||||
@endif
|
||||
</div>
|
||||
<flux:button href="/customers/{{ $customer->id }}" variant="outline" size="sm">
|
||||
<flux:icon name="arrow-left" class="size-4" />
|
||||
Back to Customer
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
@if (session()->has('success'))
|
||||
<div class="bg-green-50 border border-green-200 text-green-800 rounded-md p-4">
|
||||
<div class="flex">
|
||||
<flux:icon name="check-circle" class="h-5 w-5 text-green-400" />
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium">{{ session('success') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Customer Edit Form -->
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
||||
<form wire:submit="updateCustomer" class="p-6 space-y-6">
|
||||
<form wire:submit="updateCustomer" class="p-6 space-y-8">
|
||||
<!-- Personal Information -->
|
||||
<div>
|
||||
<flux:heading size="lg" class="mb-4">Personal Information</flux:heading>
|
||||
@ -142,6 +139,97 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Account Management -->
|
||||
<div>
|
||||
<flux:heading size="lg" class="mb-4">Customer Portal Access</flux:heading>
|
||||
|
||||
@if($has_user_account)
|
||||
<!-- Existing User Account -->
|
||||
@if($customer->user && $customer->user->isCustomer())
|
||||
<div class="bg-green-50 dark:bg-green-900/20 p-4 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<div class="flex items-center mb-4">
|
||||
<flux:icon name="check-circle" class="h-5 w-5 text-green-600 mr-2" />
|
||||
<span class="font-medium text-green-800 dark:text-green-200">Customer has active portal access</span>
|
||||
</div>
|
||||
@else
|
||||
<div class="bg-yellow-50 dark:bg-yellow-900/20 p-4 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
||||
<div class="flex items-center mb-4">
|
||||
<flux:icon name="exclamation-triangle" class="h-5 w-5 text-yellow-600 mr-2" />
|
||||
<span class="font-medium text-yellow-800 dark:text-yellow-200">Customer portal access is inactive</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<flux:field>
|
||||
<flux:label>User Status</flux:label>
|
||||
<select wire:model="user_status" class="w-full rounded-md border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
</select>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<flux:field>
|
||||
<flux:checkbox wire:model.live="reset_password" />
|
||||
<flux:label>Reset password</flux:label>
|
||||
<flux:description>Generate new password for customer portal</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($reset_password)
|
||||
<div class="mt-4">
|
||||
<flux:field>
|
||||
<flux:label>New Password</flux:label>
|
||||
<div class="flex space-x-2">
|
||||
<flux:input wire:model="new_password" type="text" placeholder="Generated password" class="flex-1" />
|
||||
<flux:button type="button" wire:click="generatePassword" variant="outline" size="sm">
|
||||
Generate
|
||||
</flux:button>
|
||||
</div>
|
||||
<flux:error name="new_password" />
|
||||
<flux:description>Customer will need to use this password to login</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<!-- No User Account -->
|
||||
<div class="bg-yellow-50 dark:bg-yellow-900/20 p-4 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
||||
<div class="flex items-center mb-4">
|
||||
<flux:icon name="exclamation-triangle" class="h-5 w-5 text-yellow-600 mr-2" />
|
||||
<span class="font-medium text-yellow-800 dark:text-yellow-200">Customer has no portal access</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<flux:field>
|
||||
<flux:checkbox wire:model.live="create_user_account" />
|
||||
<flux:label>Create customer portal account</flux:label>
|
||||
<flux:description>Allow this customer to login and access their portal</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
@if($create_user_account)
|
||||
<div class="mt-4">
|
||||
<flux:field>
|
||||
<flux:label>Password</flux:label>
|
||||
<div class="flex space-x-2">
|
||||
<flux:input wire:model="new_password" type="text" placeholder="Generated password" class="flex-1" />
|
||||
<flux:button type="button" wire:click="generatePassword" variant="outline" size="sm">
|
||||
Generate
|
||||
</flux:button>
|
||||
</div>
|
||||
<flux:error name="new_password" />
|
||||
<flux:description>Customer will receive this password to access their portal</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="flex items-center justify-between pt-6 border-t border-zinc-200 dark:border-zinc-700">
|
||||
<flux:button href="/customers/{{ $customer->id }}" variant="outline">
|
||||
|
||||
@ -79,6 +79,7 @@
|
||||
</th>
|
||||
<th class="text-left py-3 px-4">Address</th>
|
||||
<th class="text-left py-3 px-4">Vehicles</th>
|
||||
<th class="text-left py-3 px-4">Portal Access</th>
|
||||
<th class="text-left py-3 px-4">
|
||||
<button wire:click="sortBy('last_service_date')" class="flex items-center space-x-1 hover:text-blue-600">
|
||||
<span>Last Service</span>
|
||||
@ -118,6 +119,19 @@
|
||||
</flux:badge>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
@if($customer->user && $customer->user->isCustomer())
|
||||
<flux:badge variant="success" size="sm">
|
||||
<flux:icon name="check-circle" class="size-3 mr-1" />
|
||||
Active
|
||||
</flux:badge>
|
||||
@else
|
||||
<flux:badge variant="outline" size="sm">
|
||||
<flux:icon name="x-circle" class="size-3 mr-1" />
|
||||
No Access
|
||||
</flux:badge>
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<div class="text-sm">
|
||||
@if($customer->last_service_date)
|
||||
|
||||
@ -3,7 +3,13 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ $customer->full_name }}</flux:heading>
|
||||
<flux:subheading>Customer #{{ $customer->id }} - {{ ucfirst($customer->status) }}</flux:subheading>
|
||||
<flux:subheading>Customer #{{ $customer->id }} - {{ ucfirst($customer->status) }}
|
||||
@if($customer->user && $customer->user->isCustomer())
|
||||
<flux:badge variant="success" size="sm" class="ml-2">Portal Access</flux:badge>
|
||||
@else
|
||||
<flux:badge variant="outline" size="sm" class="ml-2">No Portal</flux:badge>
|
||||
@endif
|
||||
</flux:subheading>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<flux:button href="/customers-list" variant="outline" size="sm">
|
||||
@ -21,6 +27,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Customer Stats -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-6 gap-4">
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 p-4">
|
||||
<div class="text-2xl font-bold text-blue-600">{{ $stats['total_vehicles'] }}</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">Vehicles</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 p-4">
|
||||
<div class="text-2xl font-bold text-green-600">{{ $stats['total_service_orders'] }}</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">Service Orders</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 p-4">
|
||||
<div class="text-2xl font-bold text-purple-600">{{ $stats['total_appointments'] }}</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">Appointments</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 p-4">
|
||||
<div class="text-2xl font-bold text-orange-600">{{ $stats['active_job_cards'] }}</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">Active Jobs</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 p-4">
|
||||
<div class="text-2xl font-bold text-teal-600">{{ $stats['completed_services'] }}</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">Completed</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 p-4">
|
||||
<div class="text-2xl font-bold text-indigo-600">${{ number_format($stats['total_spent'], 2) }}</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">Total Spent</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Customer Information -->
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
@ -75,6 +109,68 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Account Information -->
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
||||
<div class="border-b border-zinc-200 dark:border-zinc-700 p-4">
|
||||
<flux:heading size="lg">Customer Portal Access</flux:heading>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
@if($customer->user && $customer->user->isCustomer())
|
||||
<div class="flex items-center space-x-3 mb-4">
|
||||
<flux:icon name="check-circle" class="h-6 w-6 text-green-600" />
|
||||
<div>
|
||||
<div class="font-medium text-green-800 dark:text-green-200">Customer has portal access</div>
|
||||
<div class="text-sm text-green-600 dark:text-green-400">Can login and view service history</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<flux:label>User ID</flux:label>
|
||||
<div class="mt-1 text-sm">#{{ $customer->user->id }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<flux:label>Account Status</flux:label>
|
||||
<div class="mt-1">
|
||||
@if($customer->user->status === 'active')
|
||||
<flux:badge variant="success" size="sm">Active</flux:badge>
|
||||
@else
|
||||
<flux:badge variant="danger" size="sm">Inactive</flux:badge>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<flux:label>Last Login</flux:label>
|
||||
<div class="mt-1 text-sm">
|
||||
{{ $customer->user->last_login_at ? $customer->user->last_login_at->format('M j, Y g:i A') : 'Never' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-4 border-t border-zinc-200 dark:border-zinc-700">
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Portal URL: <a href="/customer-portal" class="text-blue-600 hover:underline">/customer-portal</a>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex items-center space-x-3">
|
||||
<flux:icon name="exclamation-triangle" class="h-6 w-6 text-yellow-600" />
|
||||
<div>
|
||||
<div class="font-medium text-yellow-800 dark:text-yellow-200">No portal access</div>
|
||||
<div class="text-sm text-yellow-600 dark:text-yellow-400">Customer cannot login to view their information</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<flux:button href="/customers/{{ $customer->id }}/edit" variant="outline" size="sm">
|
||||
<flux:icon name="plus" class="size-4" />
|
||||
Create Portal Account
|
||||
</flux:button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vehicles -->
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
||||
<div class="border-b border-zinc-200 dark:border-zinc-700 p-4 flex items-center justify-between">
|
||||
|
||||
@ -1,3 +1,258 @@
|
||||
<div>
|
||||
{{-- The whole world belongs to you. --}}
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||
{{ __('Timesheets') }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<!-- Header with Actions -->
|
||||
<div class="mb-6 flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Timesheets</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">Track technician work hours and job progress</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<button wire:click="$set('showCreateModal', true)"
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
New Entry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Technician</label>
|
||||
<select wire:model.live="selectedTechnician" class="w-full border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:text-white">
|
||||
<option value="">All Technicians</option>
|
||||
@foreach($technicians as $technician)
|
||||
<option value="{{ $technician->id }}">{{ $technician->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Date From</label>
|
||||
<input type="date" wire:model.live="dateFrom" class="w-full border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Date To</label>
|
||||
<input type="date" wire:model.live="dateTo" class="w-full border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Status</label>
|
||||
<select wire:model.live="selectedStatus" class="w-full border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:text-white">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="draft">Draft</option>
|
||||
<option value="submitted">Submitted</option>
|
||||
<option value="approved">Approved</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timesheets Table -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
Technician
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
Job Card
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
Date
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
Start Time
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
End Time
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
Hours
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
@forelse($timesheets as $timesheet)
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-8 w-8">
|
||||
<div class="h-8 w-8 rounded-full bg-blue-500 flex items-center justify-center text-white text-sm font-medium">
|
||||
{{ substr($timesheet->user->name, 0, 1) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ $timesheet->user->name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900 dark:text-white">
|
||||
@if($timesheet->job_card_id)
|
||||
<a href="{{ route('job-cards.show', $timesheet->job_card_id) }}" class="text-blue-600 hover:text-blue-800">
|
||||
#{{ $timesheet->job_card_id }}
|
||||
</a>
|
||||
@else
|
||||
<span class="text-gray-500">-</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
||||
{{ $timesheet->date->format('M j, Y') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
||||
{{ $timesheet->start_time ? $timesheet->start_time->format('g:i A') : '-' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
||||
{{ $timesheet->end_time ? $timesheet->end_time->format('g:i A') : '-' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
||||
{{ number_format($timesheet->hours_worked, 2) }}h
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full
|
||||
@if($timesheet->status === 'active') bg-green-100 text-green-800
|
||||
@elseif($timesheet->status === 'completed') bg-blue-100 text-blue-800
|
||||
@elseif($timesheet->status === 'pending') bg-yellow-100 text-yellow-800
|
||||
@else bg-gray-100 text-gray-800 @endif">
|
||||
{{ ucfirst($timesheet->status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div class="flex space-x-2">
|
||||
<button wire:click="editTimesheet({{ $timesheet->id }})"
|
||||
class="text-indigo-600 hover:text-indigo-900">
|
||||
Edit
|
||||
</button>
|
||||
<button wire:click="deleteTimesheet({{ $timesheet->id }})"
|
||||
class="text-red-600 hover:text-red-900"
|
||||
onclick="return confirm('Are you sure?')">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8" class="px-6 py-12 text-center">
|
||||
<div class="text-gray-500 dark:text-gray-400">
|
||||
<svg class="mx-auto h-12 w-12 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium mb-2">No timesheets found</h3>
|
||||
<p class="mb-4">Get started by creating your first timesheet entry.</p>
|
||||
<button wire:click="$set('showCreateModal', true)"
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg">
|
||||
Create First Entry
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if($timesheets->hasPages())
|
||||
<div class="px-6 py-3 border-t border-gray-200 dark:border-gray-700">
|
||||
{{ $timesheets->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create/Edit Modal -->
|
||||
@if($showCreateModal || $showEditModal)
|
||||
<div class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white dark:bg-gray-800">
|
||||
<div class="mt-3">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
{{ $showCreateModal ? 'Create Timesheet Entry' : 'Edit Timesheet Entry' }}
|
||||
</h3>
|
||||
|
||||
<form wire:submit.prevent="{{ $showCreateModal ? 'createTimesheet' : 'updateTimesheet' }}">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Technician</label>
|
||||
<select wire:model="form.user_id" class="w-full border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:text-white" required>
|
||||
<option value="">Select Technician</option>
|
||||
@foreach($technicians as $technician)
|
||||
<option value="{{ $technician->id }}">{{ $technician->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('form.technician_id') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Job Card (Optional)</label>
|
||||
<select wire:model="form.job_card_id" class="w-full border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:text-white">
|
||||
<option value="">Select Job Card</option>
|
||||
@foreach($jobCards as $jobCard)
|
||||
<option value="{{ $jobCard->id }}">#{{ $jobCard->id }} - {{ $jobCard->vehicle->make }} {{ $jobCard->vehicle->model }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('form.job_card_id') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Date</label>
|
||||
<input type="date" wire:model="form.date" class="w-full border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:text-white" required>
|
||||
@error('form.date') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Start Time</label>
|
||||
<input type="time" wire:model="form.start_time" class="w-full border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:text-white" required>
|
||||
@error('form.start_time') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">End Time</label>
|
||||
<input type="time" wire:model="form.end_time" class="w-full border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:text-white">
|
||||
@error('form.end_time') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Description</label>
|
||||
<textarea wire:model="form.description" rows="3" class="w-full border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:text-white" placeholder="What work was performed?"></textarea>
|
||||
@error('form.description') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 mt-6">
|
||||
<button type="button" wire:click="closeModal" class="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
|
||||
{{ $showCreateModal ? 'Create' : 'Update' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@ -1,86 +1,82 @@
|
||||
<div class="p-6">
|
||||
<!-- Header with Stats -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col lg:flex-row lg:items-center justify-between space-y-4 lg:space-y-0 mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-zinc-900 dark:text-white">User Management</h1>
|
||||
<h1 class="text-2xl font-semibold text-zinc-900 dark:text-white">Users Management</h1>
|
||||
<p class="text-zinc-600 dark:text-zinc-400">Manage system users, roles, and permissions</p>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<div class="flex items-center space-x-6 mt-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||
<span class="text-sm text-zinc-600 dark:text-zinc-400">Active: {{ $stats['active'] }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-3 h-3 bg-gray-500 rounded-full"></div>
|
||||
<span class="text-sm text-zinc-600 dark:text-zinc-400">Inactive: {{ $stats['inactive'] }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-3 h-3 bg-red-500 rounded-full"></div>
|
||||
<span class="text-sm text-zinc-600 dark:text-zinc-400">Suspended: {{ $stats['suspended'] }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-3 h-3 bg-blue-500 rounded-full"></div>
|
||||
<span class="text-sm text-zinc-600 dark:text-zinc-400">Total: {{ $stats['total'] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
@if($this->getSelectedCount() > 0)
|
||||
<!-- Bulk Actions -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm text-zinc-600 dark:text-zinc-400">{{ $this->getSelectedCount() }} selected</span>
|
||||
<button wire:click="bulkActivate"
|
||||
class="inline-flex items-center px-3 py-1.5 bg-green-600 text-white text-xs font-medium rounded hover:bg-green-700 transition-colors">
|
||||
Activate
|
||||
</button>
|
||||
<button wire:click="bulkDeactivate"
|
||||
class="inline-flex items-center px-3 py-1.5 bg-gray-600 text-white text-xs font-medium rounded hover:bg-gray-700 transition-colors">
|
||||
Deactivate
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<button wire:click="exportUsers"
|
||||
class="inline-flex items-center px-3 py-2 bg-gray-600 text-white text-sm font-medium rounded-md hover:bg-gray-700 transition-colors">
|
||||
class="inline-flex items-center px-4 py-2 text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-700 text-sm font-medium rounded-md hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
Export
|
||||
</button>
|
||||
|
||||
<a href="{{ route('users.create') }}"
|
||||
class="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
|
||||
class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors"
|
||||
wire:navigate>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||
</svg>
|
||||
Add New User
|
||||
Add User
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters and Search -->
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6">
|
||||
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
|
||||
<div class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Total Users</div>
|
||||
<div class="text-2xl font-bold text-zinc-900 dark:text-white">{{ $stats['total'] }}</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
|
||||
<div class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Active</div>
|
||||
<div class="text-2xl font-bold text-green-600">{{ $stats['active'] }}</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
|
||||
<div class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Inactive</div>
|
||||
<div class="text-2xl font-bold text-gray-600">{{ $stats['inactive'] }}</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
|
||||
<div class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Suspended</div>
|
||||
<div class="text-2xl font-bold text-red-600">{{ $stats['suspended'] }}</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
|
||||
<div class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Customers</div>
|
||||
<div class="text-2xl font-bold text-purple-600">{{ $stats['customers'] }}</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
|
||||
<div class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Staff</div>
|
||||
<div class="text-2xl font-bold text-blue-600">{{ $stats['staff'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6 mb-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-4">
|
||||
<div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 gap-4">
|
||||
<!-- Search -->
|
||||
<div class="col-span-1 md:col-span-2">
|
||||
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Search</label>
|
||||
<input type="text"
|
||||
wire:model.live.debounce.300ms="search"
|
||||
placeholder="Name, email, employee ID, phone..."
|
||||
placeholder="Search by name, email, or employee ID..."
|
||||
class="w-full rounded-md border-zinc-300 dark:border-zinc-600 dark:bg-zinc-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<!-- Role Filter -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Role</label>
|
||||
<select wire:model.live="roleFilter"
|
||||
class="w-full rounded-md border-zinc-300 dark:border-zinc-600 dark:bg-zinc-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">All Roles</option>
|
||||
@foreach($roles as $role)
|
||||
<option value="{{ $role->name }}">{{ $role->display_name }}</option>
|
||||
<option value="{{ $role->name }}">{{ $role->display_name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Status Filter -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Status</label>
|
||||
<select wire:model.live="statusFilter"
|
||||
@ -91,235 +87,277 @@
|
||||
<option value="suspended">Suspended</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Department Filter -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Department</label>
|
||||
<select wire:model.live="departmentFilter"
|
||||
class="w-full rounded-md border-zinc-300 dark:border-zinc-600 dark:bg-zinc-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">All Departments</option>
|
||||
@foreach($departments as $department)
|
||||
<option value="{{ $department }}">{{ ucfirst(str_replace('_', ' ', $department)) }}</option>
|
||||
<option value="{{ $department }}">{{ $department }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Branch Filter -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Branch</label>
|
||||
<select wire:model.live="branchFilter"
|
||||
class="w-full rounded-md border-zinc-300 dark:border-zinc-600 dark:bg-zinc-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">All Branches</option>
|
||||
@foreach($branches as $branch)
|
||||
<option value="{{ $branch }}">{{ ucfirst(str_replace('_', ' ', $branch)) }}</option>
|
||||
<option value="{{ $branch }}">{{ $branch }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Customer Filter -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Customer Type</label>
|
||||
<select wire:model.live="customerFilter"
|
||||
class="w-full rounded-md border-zinc-300 dark:border-zinc-600 dark:bg-zinc-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">All Users</option>
|
||||
<option value="customers_only">Customers Only</option>
|
||||
<option value="non_customers">Staff Only</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Options -->
|
||||
<div class="flex items-center justify-between">
|
||||
|
||||
<!-- Filter Actions -->
|
||||
<div class="flex items-center justify-between mt-4 pt-4 border-t border-zinc-200 dark:border-zinc-700">
|
||||
<div class="flex items-center space-x-4">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" wire:model.live="showInactive"
|
||||
<input type="checkbox"
|
||||
wire:model.live="showInactive"
|
||||
class="rounded border-zinc-300 dark:border-zinc-600 text-blue-600 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<span class="ml-2 text-sm text-zinc-700 dark:text-zinc-300">Show inactive users</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@if($this->hasActiveFilters())
|
||||
<button wire:click="clearFilters"
|
||||
class="inline-flex items-center px-3 py-1.5 text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 border border-blue-200 dark:border-blue-800 rounded-md hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
Clear All Filters
|
||||
class="text-sm text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200">
|
||||
Clear filters
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Info and Per Page -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<select wire:model.live="perPage"
|
||||
class="rounded-md border-zinc-300 dark:border-zinc-600 dark:bg-zinc-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="10">10 per page</option>
|
||||
<option value="25">25 per page</option>
|
||||
<option value="50">50 per page</option>
|
||||
<option value="100">100 per page</option>
|
||||
</select>
|
||||
<span class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Showing {{ $users->firstItem() ?? 0 }} to {{ $users->lastItem() ?? 0 }} of {{ $users->total() }} users
|
||||
</span>
|
||||
<!-- Bulk Actions -->
|
||||
@if(!empty($selectedUsers))
|
||||
<div class="bg-blue-50 dark:bg-blue-900/50 border border-blue-200 dark:border-blue-700 rounded-lg p-4 mb-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm text-blue-800 dark:text-blue-200">
|
||||
{{ count($selectedUsers) }} user(s) selected
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button wire:click="bulkActivate"
|
||||
class="text-sm bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded-md transition-colors">
|
||||
Activate
|
||||
</button>
|
||||
<button wire:click="bulkDeactivate"
|
||||
class="text-sm bg-gray-600 hover:bg-gray-700 text-white px-3 py-1 rounded-md transition-colors">
|
||||
Deactivate
|
||||
</button>
|
||||
<button wire:click="$set('selectedUsers', [])"
|
||||
class="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200">
|
||||
Clear selection
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Users Table -->
|
||||
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg overflow-hidden">
|
||||
<!-- Table Header -->
|
||||
<div class="px-6 py-4 border-b border-zinc-200 dark:border-zinc-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium text-zinc-900 dark:text-white">
|
||||
Users ({{ $users->total() }})
|
||||
</h3>
|
||||
<div class="flex items-center space-x-2">
|
||||
<label class="text-sm text-zinc-600 dark:text-zinc-400">Per page:</label>
|
||||
<select wire:model.live="perPage"
|
||||
class="text-sm rounded border-zinc-300 dark:border-zinc-600 dark:bg-zinc-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table Content -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-zinc-200 dark:divide-zinc-700">
|
||||
<thead class="bg-zinc-50 dark:bg-zinc-900">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left">
|
||||
<input type="checkbox" wire:model="selectAll" wire:click="selectAllUsers"
|
||||
<input type="checkbox"
|
||||
wire:model="selectAll"
|
||||
wire:click="selectAllUsers"
|
||||
class="rounded border-zinc-300 dark:border-zinc-600 text-blue-600 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider cursor-pointer hover:bg-zinc-100 dark:hover:bg-zinc-800"
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider cursor-pointer"
|
||||
wire:click="sortBy('name')">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span>Name</span>
|
||||
<span>User</span>
|
||||
@if($sortField === 'name')
|
||||
@if($sortDirection === 'asc')
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path>
|
||||
</svg>
|
||||
@else
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
@endif
|
||||
<svg class="w-4 h-4 {{ $sortDirection === 'asc' ? 'rotate-180' : '' }}" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider cursor-pointer hover:bg-zinc-100 dark:hover:bg-zinc-800"
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider cursor-pointer"
|
||||
wire:click="sortBy('email')">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span>Email</span>
|
||||
<span>Contact</span>
|
||||
@if($sortField === 'email')
|
||||
@if($sortDirection === 'asc')
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path>
|
||||
</svg>
|
||||
@else
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
@endif
|
||||
<svg class="w-4 h-4 {{ $sortDirection === 'asc' ? 'rotate-180' : '' }}" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Employee ID</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Roles</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Department</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Permissions</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Actions</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider cursor-pointer"
|
||||
wire:click="sortBy('status')">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span>Status</span>
|
||||
@if($sortField === 'status')
|
||||
<svg class="w-4 h-4 {{ $sortDirection === 'asc' ? 'rotate-180' : '' }}" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider cursor-pointer"
|
||||
wire:click="sortBy('created_at')">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span>Created</span>
|
||||
@if($sortField === 'created_at')
|
||||
<svg class="w-4 h-4 {{ $sortDirection === 'asc' ? 'rotate-180' : '' }}" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-zinc-800 divide-y divide-zinc-200 dark:divide-zinc-700">
|
||||
@forelse($users as $user)
|
||||
<tr class="hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors {{ in_array($user->id, $selectedUsers) ? 'bg-blue-50 dark:bg-blue-900/20' : '' }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<input type="checkbox" wire:model="selectedUsers" value="{{ $user->id }}"
|
||||
<tr class="hover:bg-zinc-50 dark:hover:bg-zinc-700/50">
|
||||
<td class="px-6 py-4">
|
||||
<input type="checkbox"
|
||||
wire:model="selectedUsers"
|
||||
value="{{ $user->id }}"
|
||||
class="rounded border-zinc-300 dark:border-zinc-600 text-blue-600 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 w-10 h-10 bg-zinc-200 dark:bg-zinc-600 rounded-lg flex items-center justify-center text-zinc-600 dark:text-zinc-300 font-semibold text-sm">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="flex-shrink-0 w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center text-white font-medium text-sm">
|
||||
{{ $user->initials() }}
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-sm font-medium text-zinc-900 dark:text-white">{{ $user->name }}</div>
|
||||
@if($user->position)
|
||||
<div class="text-sm text-zinc-500 dark:text-zinc-400">{{ $user->position }}</div>
|
||||
<div>
|
||||
<div class="text-sm font-medium text-zinc-900 dark:text-white">
|
||||
<a href="{{ route('users.show', $user) }}"
|
||||
class="hover:text-blue-600 dark:hover:text-blue-400"
|
||||
wire:navigate>
|
||||
{{ $user->name }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{{ $user->position ?? 'User' }}
|
||||
@if($user->department)
|
||||
• {{ $user->department }}
|
||||
@endif
|
||||
</div>
|
||||
@if($user->employee_id)
|
||||
<div class="text-xs font-mono text-zinc-400 dark:text-zinc-500">
|
||||
ID: {{ $user->employee_id }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-zinc-900 dark:text-white">{{ $user->email }}</div>
|
||||
@if($user->phone)
|
||||
<div class="text-sm text-zinc-500 dark:text-zinc-400">{{ $user->phone }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{{ $user->employee_id ?: '-' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
@if($user->roles->count() > 0)
|
||||
<div class="flex flex-wrap gap-1">
|
||||
@foreach($user->roles->take(2) as $role)
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $this->getRoleBadgeClass($role->name) }}">
|
||||
{{ $role->display_name }}
|
||||
</span>
|
||||
@endforeach
|
||||
@if($user->roles->count() > 2)
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-zinc-100 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-300">
|
||||
+{{ $user->roles->count() - 2 }} more
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<span class="text-sm text-zinc-500 dark:text-zinc-400">No roles</span>
|
||||
@if($user->branch_code)
|
||||
<div class="text-xs text-zinc-400 dark:text-zinc-500">Branch: {{ $user->branch_code }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{{ $user->department ? ucfirst(str_replace('_', ' ', $user->department)) : '-' }}
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
@foreach($user->activeRoles() as $role)
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium {{ $this->getRoleBadgeClass($role->name) }}">
|
||||
{{ $role->display_name }}
|
||||
</span>
|
||||
@endforeach
|
||||
@if($user->customer)
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200">
|
||||
Customer
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<td class="px-6 py-4">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $this->getStatusBadgeClass($user->status) }}">
|
||||
{{ ucfirst($user->status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200">
|
||||
{{ $this->getUserPermissionCount($user) }}
|
||||
</span>
|
||||
<td class="px-6 py-4 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{{ $user->created_at->format('M j, Y') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div class="flex items-center space-x-2">
|
||||
<td class="px-6 py-4 text-right text-sm font-medium">
|
||||
<div class="flex items-center justify-end space-x-2">
|
||||
<a href="{{ route('users.show', $user) }}"
|
||||
class="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 p-1 rounded"
|
||||
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200"
|
||||
wire:navigate
|
||||
title="View User">
|
||||
title="View">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('users.edit', $user) }}"
|
||||
class="text-amber-600 dark:text-amber-400 hover:text-amber-700 dark:hover:text-amber-300 p-1 rounded"
|
||||
class="text-zinc-600 dark:text-zinc-400 hover:text-zinc-800 dark:hover:text-zinc-200"
|
||||
wire:navigate
|
||||
title="Edit User">
|
||||
title="Edit">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('users.manage-roles', $user) }}"
|
||||
class="text-purple-600 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300 p-1 rounded"
|
||||
wire:navigate
|
||||
title="Manage Roles">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
@if($user->id !== auth()->id())
|
||||
@if($user->status === 'active')
|
||||
<button wire:click="deactivateUser({{ $user->id }})"
|
||||
class="text-gray-600 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 p-1 rounded"
|
||||
title="Deactivate User"
|
||||
onclick="confirm('Are you sure you want to deactivate this user?') || event.stopImmediatePropagation()">
|
||||
class="text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200"
|
||||
title="Deactivate">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18.364 5.636M5.636 18.364l12.728-12.728"></path>
|
||||
</svg>
|
||||
</button>
|
||||
@elseif($user->status === 'inactive')
|
||||
<button wire:click="activateUser({{ $user->id }})"
|
||||
class="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 p-1 rounded"
|
||||
title="Activate User">
|
||||
class="text-green-600 dark:text-green-400 hover:text-green-800 dark:hover:text-green-200"
|
||||
title="Activate">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
|
||||
|
||||
@if($user->status !== 'suspended')
|
||||
<button wire:click="suspendUser({{ $user->id }})"
|
||||
class="text-orange-600 dark:text-orange-400 hover:text-orange-700 dark:hover:text-orange-300 p-1 rounded"
|
||||
title="Suspend User"
|
||||
onclick="confirm('Are you sure you want to suspend this user?') || event.stopImmediatePropagation()">
|
||||
class="text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-200"
|
||||
title="Suspend">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 15.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636"></path>
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
@ -329,18 +367,30 @@
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="9" class="px-6 py-12 text-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<svg class="w-12 h-12 text-zinc-400 dark:text-zinc-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
||||
<td colspan="7" class="px-6 py-12 text-center">
|
||||
<div class="text-zinc-500 dark:text-zinc-400">
|
||||
<svg class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-zinc-900 dark:text-white mb-2">No users found</h3>
|
||||
<p class="text-zinc-500 dark:text-zinc-400 mb-4">Try adjusting your search or filter criteria.</p>
|
||||
@if($this->hasActiveFilters())
|
||||
<button wire:click="clearFilters"
|
||||
class="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 transition-colors">
|
||||
Clear All Filters
|
||||
</button>
|
||||
<h3 class="mt-2 text-sm font-medium text-zinc-900 dark:text-zinc-100">No users found</h3>
|
||||
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
@if($search || $roleFilter || $statusFilter || $departmentFilter || $branchFilter || $customerFilter)
|
||||
Try adjusting your filters to find more users.
|
||||
@else
|
||||
Get started by creating a new user.
|
||||
@endif
|
||||
</p>
|
||||
@if(!$search && !$roleFilter && !$statusFilter && !$departmentFilter && !$branchFilter && !$customerFilter)
|
||||
<div class="mt-6">
|
||||
<a href="{{ route('users.create') }}"
|
||||
class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors"
|
||||
wire:navigate>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||
</svg>
|
||||
Add your first user
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
@ -349,12 +399,12 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if($users->hasPages())
|
||||
<div class="mt-6">
|
||||
{{ $users->links() }}
|
||||
<!-- Pagination -->
|
||||
@if($users->hasPages())
|
||||
<div class="px-6 py-4 border-t border-zinc-200 dark:border-zinc-700">
|
||||
{{ $users->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@ -3,15 +3,66 @@
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-zinc-900 dark:text-white">Manage Roles & Permissions</h1>
|
||||
<p class="text-zinc-600 dark:text-zinc-400">Configure access control for {{ $user->name }}</p>
|
||||
<p class="text-zinc-600 dark:text-zinc-400">
|
||||
Configure access control for {{ $user->name }}
|
||||
@if($user->customer)
|
||||
<span class="inline-flex items-center px-2 py-1 ml-2 text-xs font-medium bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded">
|
||||
Customer Account
|
||||
</span>
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
@if($user->customer)
|
||||
<a href="{{ route('customers.show', $user->customer) }}"
|
||||
class="inline-flex items-center px-4 py-2 text-purple-700 dark:text-purple-300 bg-purple-100 dark:bg-purple-700 text-sm font-medium rounded-md hover:bg-purple-200 dark:hover:bg-purple-600 transition-colors"
|
||||
wire:navigate>
|
||||
View Customer
|
||||
</a>
|
||||
@endif
|
||||
<a href="{{ route('users.show', $user) }}"
|
||||
class="inline-flex items-center px-4 py-2 text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-700 text-sm font-medium rounded-md hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors"
|
||||
wire:navigate>
|
||||
Back to User
|
||||
</a>
|
||||
</div>
|
||||
<a href="{{ route('users.show', $user) }}"
|
||||
class="inline-flex items-center px-4 py-2 text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-700 text-sm font-medium rounded-md hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors"
|
||||
wire:navigate>
|
||||
Back to User
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if($user->customer)
|
||||
<!-- Customer Portal Notice -->
|
||||
<div class="bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg p-4 mb-6">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-purple-800 dark:text-purple-200">Customer Account</h3>
|
||||
<p class="text-sm text-purple-600 dark:text-purple-400">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@unless($user->hasRole('customer_portal'))
|
||||
<!-- Customer Portal Role Warning -->
|
||||
<div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4 mb-6">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 text-yellow-600 dark:text-yellow-400 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 15c-.77.833.192 2.5 1.732 2.5z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">Missing Customer Portal Access</h3>
|
||||
<p class="text-sm text-yellow-600 dark:text-yellow-400">
|
||||
This customer user doesn't have the "Customer Portal" role. Click the preset above to grant customer portal access.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endunless
|
||||
@endif
|
||||
|
||||
<!-- Role Management -->
|
||||
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6 mb-6">
|
||||
<h3 class="text-lg font-medium text-zinc-900 dark:text-white mb-4">Role Assignment</h3>
|
||||
@ -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
|
||||
</button>
|
||||
<button type="button"
|
||||
wire:click="applyRolePreset('customer_portal')"
|
||||
class="px-3 py-1 text-xs font-medium text-pink-600 dark:text-pink-400 bg-pink-50 dark:bg-pink-900/20 rounded-md hover:bg-pink-100 dark:hover:bg-pink-900/30 transition-colors">
|
||||
Customer Portal
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -45,12 +45,23 @@
|
||||
Branch: {{ $user->branch_code }}
|
||||
</span>
|
||||
@endif
|
||||
@if($user->customer)
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200">
|
||||
Customer Account
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex items-center space-x-3">
|
||||
@if($user->customer)
|
||||
<flux:button href="{{ route('customers.show', $user->customer) }}" wire:navigate variant="outline" size="sm" icon="user">
|
||||
View Customer
|
||||
</flux:button>
|
||||
@endif
|
||||
|
||||
@if($this->canPerformAction('impersonate'))
|
||||
<flux:button variant="outline" size="sm" wire:click="confirmImpersonate" icon="user-circle">
|
||||
Impersonate
|
||||
@ -75,6 +86,80 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Management Submenu -->
|
||||
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg">
|
||||
<div class="flex items-center justify-between p-4 border-b border-zinc-200 dark:border-zinc-700">
|
||||
<h2 class="text-lg font-medium text-zinc-900 dark:text-white">User Management</h2>
|
||||
<div class="flex items-center space-x-1">
|
||||
<button class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-zinc-700 rounded-md transition-colors">
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
||||
</svg>
|
||||
User Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-1 p-4">
|
||||
<a href="{{ route('users.index') }}"
|
||||
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-zinc-700 rounded-md transition-colors"
|
||||
wire:navigate>
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z"></path>
|
||||
</svg>
|
||||
All Users
|
||||
</a>
|
||||
|
||||
<a href="{{ route('users.show', $user) }}"
|
||||
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20 rounded-md border border-blue-200 dark:border-blue-800"
|
||||
wire:navigate>
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
||||
</svg>
|
||||
User Details
|
||||
</a>
|
||||
|
||||
<a href="{{ route('users.manage-roles', $user) }}"
|
||||
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-zinc-700 rounded-md transition-colors"
|
||||
wire:navigate>
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
|
||||
</svg>
|
||||
Roles & Permissions
|
||||
</a>
|
||||
|
||||
@if($user->customer)
|
||||
<a href="{{ route('customers.show', $user->customer) }}"
|
||||
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-purple-600 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300 hover:bg-purple-50 dark:hover:bg-purple-900/20 rounded-md transition-colors"
|
||||
wire:navigate>
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
||||
</svg>
|
||||
Customer Profile
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<a href="{{ route('users.edit', $user) }}"
|
||||
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-zinc-700 rounded-md transition-colors"
|
||||
wire:navigate>
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||
</svg>
|
||||
Edit User
|
||||
</a>
|
||||
|
||||
<div class="border-l border-zinc-200 dark:border-zinc-600 mx-2 h-6"></div>
|
||||
|
||||
<button onclick="showUserActionsMenu()"
|
||||
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-zinc-700 rounded-md transition-colors">
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"></path>
|
||||
</svg>
|
||||
Actions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
|
||||
|
||||
@ -104,6 +104,16 @@
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Shop Logo</label>
|
||||
<input type="file" name="shop_logo" accept="image/*" class="w-full px-3 py-2 border border-zinc-300 dark:border-zinc-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-zinc-700 dark:text-white" />
|
||||
@if(!empty($settings->shop_logo))
|
||||
<div class="mt-2">
|
||||
<img src="{{ asset($settings->shop_logo) }}" alt="Shop Logo" class="h-12 rounded-md border border-zinc-300 dark:border-zinc-600" />
|
||||
</div>
|
||||
@endif
|
||||
@error('shop_logo')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Default Tax Rate (%) *</label>
|
||||
<input type="number" step="0.01" min="0" max="100" name="default_tax_rate"
|
||||
value="{{ old('default_tax_rate', $settings->default_tax_rate ?? '0.00') }}" required
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/test-permissions', function () {
|
||||
$user = auth()->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');
|
||||
@ -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';
|
||||
|
||||
Reference in New Issue
Block a user