Add customer portal views for dashboard, estimates, invoices, vehicles, and work orders
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled

- 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:
2025-08-08 09:56:26 +00:00
parent 6b3baffc3e
commit cbae4564b9
63 changed files with 4276 additions and 885 deletions

View 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.");
}
}

View 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("");
}
}
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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!");
}
}
}

View 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;
}
}

View 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;
}
}

View 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());
}
}
}

View 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;
}
}

View 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);
}
}

View 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');
}
}

View 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');
}
}

View 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');
}
}

View 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();
}
}

View 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');
}
}

View 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');
}
}

View File

@ -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()

View File

@ -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);
}

View File

@ -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 . '%')

View File

@ -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()

View File

@ -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'));
}
}

View File

@ -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',
};

View File

@ -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');
}
}

View File

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

View File

@ -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);

View File

@ -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.
*/

View File

@ -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);

View File

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

View File

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

View File

@ -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) {

View File

@ -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');
});
}
};

View File

@ -1,2 +0,0 @@
User-agent: *
Disallow:

View File

@ -0,0 +1,5 @@
<x-layouts.app.sidebar :title="$title ?? null">
<main class="flex-1">
{{ $slot }}
</main>
</x-layouts.app.sidebar>

View File

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

View File

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

View File

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

View File

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

View 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">
&copy; {{ date('Y') }} {{ $generalSettings->shop_name ?? config('app.name') }}. All rights reserved.
</p>
</div>
</div>
</footer>
</div>
</body>
</html>

View 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>

View 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>

View 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>

View 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>

View File

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

View 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>

View 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>

View File

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

View 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>

View 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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');

View File

@ -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';