['except' => ''], 'statusFilter' => ['except' => ''], 'approvalStatusFilter' => ['except' => ''], 'customerFilter' => ['except' => ''], 'dateFrom' => ['except' => ''], 'dateTo' => ['except' => ''], 'sortBy' => ['except' => 'created_at'], 'sortDirection' => ['except' => 'desc'], 'perPage' => ['except' => 15], ]; public function mount() { $this->loadStats(); } public function loadStats() { $userId = auth()->id(); // Check if user can view all estimates or only their own $canViewAll = Auth::user()->can('viewAny', Estimate::class); // Build base where clause $baseWhere = []; if (! $canViewAll) { $baseWhere['prepared_by_id'] = $userId; } // Calculate stats individually with fresh queries each time $this->stats = [ 'total' => Estimate::where($baseWhere)->count(), 'draft' => Estimate::where($baseWhere)->where('status', 'draft')->count(), 'sent' => Estimate::where($baseWhere)->where('status', 'sent')->count(), 'approved' => Estimate::where($baseWhere)->where('customer_approval_status', 'approved')->count(), 'pending' => Estimate::where($baseWhere)->whereIn('status', ['sent', 'viewed'])->where('customer_approval_status', 'pending')->count(), 'expired' => $this->getExpiredCount($canViewAll ? null : $userId), 'total_value' => Estimate::where($baseWhere)->sum('total_amount') ?: 0, 'avg_value' => Estimate::where($baseWhere)->avg('total_amount') ?: 0, ]; } private function getExpiredCount($userId = null) { $query = Estimate::where('status', '!=', 'approved') ->whereNotNull('validity_period_days'); if ($userId) { $query->where('prepared_by_id', $userId); } if (\DB::getDriverName() === 'mysql') { return $query->whereRaw('DATE_ADD(created_at, INTERVAL validity_period_days DAY) < NOW()')->count(); } else { return $query->whereRaw("datetime(created_at, '+' || validity_period_days || ' days') < datetime('now')")->count(); } } public function getDateAddExpressionForDatabase($comparison = 'expired') { // Use the actual database connection driver instead of config $databaseDriver = \DB::getDriverName(); if ($databaseDriver === 'mysql') { switch ($comparison) { case 'expired': return 'DATE_ADD(created_at, INTERVAL validity_period_days DAY) < NOW()'; case 'expiring_soon': return 'DATE_ADD(created_at, INTERVAL validity_period_days DAY) BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL 7 DAY)'; case 'valid': return 'DATE_ADD(created_at, INTERVAL validity_period_days DAY) > NOW()'; default: return 'DATE_ADD(created_at, INTERVAL validity_period_days DAY) < NOW()'; } } else { // SQLite syntax switch ($comparison) { case 'expired': return "datetime(created_at, '+' || validity_period_days || ' days') < datetime('now')"; case 'expiring_soon': return "datetime(created_at, '+' || validity_period_days || ' days') BETWEEN datetime('now') AND datetime('now', '+7 days')"; case 'valid': return "datetime(created_at, '+' || validity_period_days || ' days') > datetime('now')"; default: return "datetime(created_at, '+' || validity_period_days || ' days') < datetime('now')"; } } } public function updatedSearch() { $this->resetPage(); } public function updatedStatusFilter() { $this->resetPage(); } public function updatedApprovalStatusFilter() { $this->resetPage(); } public function updatedCustomerFilter() { $this->resetPage(); } public function updatedDateFrom() { $this->resetPage(); } public function updatedDateTo() { $this->resetPage(); } public function updatedPerPage() { $this->resetPage(); } public function sortBy($field) { if ($this->sortBy === $field) { $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc'; } else { $this->sortBy = $field; $this->sortDirection = 'asc'; } $this->resetPage(); } public function clearFilters() { $this->reset([ 'search', 'statusFilter', 'approvalStatusFilter', 'customerFilter', 'dateFrom', 'dateTo', 'totalAmountMin', 'totalAmountMax', 'validityFilter', 'branchFilter', ]); $this->resetPage(); } public function toggleAdvancedFilters() { $this->showAdvancedFilters = ! $this->showAdvancedFilters; } public function toggleBulkMode() { $this->bulkMode = ! $this->bulkMode; $this->selectedEstimates = []; $this->selectAll = false; } public function updatedSelectAll($value) { if ($value) { $this->selectedEstimates = $this->getEstimates()->pluck('id')->toArray(); } else { $this->selectedEstimates = []; } } public function bulkAction($action) { if (empty($this->selectedEstimates)) { session()->flash('error', 'Please select estimates to perform bulk action.'); return; } $estimates = Estimate::whereIn('id', $this->selectedEstimates)->get(); switch ($action) { case 'delete': $estimates->each(function ($estimate) { if (Auth::user()->can('delete', $estimate)) { $estimate->delete(); } }); session()->flash('success', count($this->selectedEstimates).' estimates deleted.'); break; case 'mark_sent': $estimates->each(function ($estimate) { if (Auth::user()->can('update', $estimate)) { $estimate->update([ 'status' => 'sent', 'sent_to_customer_at' => now(), ]); } }); session()->flash('success', count($this->selectedEstimates).' estimates marked as sent.'); break; case 'export': // Export functionality would go here session()->flash('success', 'Export started for '.count($this->selectedEstimates).' estimates.'); break; } $this->selectedEstimates = []; $this->selectAll = false; $this->bulkMode = false; $this->loadStats(); } public function getEstimates() { $query = Estimate::with([ 'jobCard.customer', 'jobCard.vehicle', 'jobCard.branch', 'customer', // For standalone estimates 'vehicle', // For standalone estimates 'preparedBy', ]); // Apply permissions if (! Auth::user()->can('viewAny', Estimate::class)) { $query->where('prepared_by_id', Auth::id()); } // Search filter if ($this->search) { $query->where(function ($q) { $q->where('estimate_number', 'like', '%'.$this->search.'%') // Search in jobCard customers (traditional estimates) ->orWhereHas('jobCard.customer', function ($customerQuery) { $customerQuery->where('first_name', 'like', '%'.$this->search.'%') ->orWhere('last_name', 'like', '%'.$this->search.'%') ->orWhere('email', 'like', '%'.$this->search.'%') ->orWhere('phone', 'like', '%'.$this->search.'%'); }) // Search in direct customers (standalone estimates) ->orWhereHas('customer', function ($customerQuery) { $customerQuery->where('first_name', 'like', '%'.$this->search.'%') ->orWhere('last_name', 'like', '%'.$this->search.'%') ->orWhere('email', 'like', '%'.$this->search.'%') ->orWhere('phone', 'like', '%'.$this->search.'%'); }) // Search in jobCard vehicles (traditional estimates) ->orWhereHas('jobCard.vehicle', function ($vehicleQuery) { $vehicleQuery->where('license_plate', 'like', '%'.$this->search.'%') ->orWhere('vin', 'like', '%'.$this->search.'%') ->orWhereRaw("CONCAT(year, ' ', make, ' ', model) LIKE ?", ['%'.$this->search.'%']); }) // Search in direct vehicles (standalone estimates) ->orWhereHas('vehicle', function ($vehicleQuery) { $vehicleQuery->where('license_plate', 'like', '%'.$this->search.'%') ->orWhere('vin', 'like', '%'.$this->search.'%') ->orWhereRaw("CONCAT(year, ' ', make, ' ', model) LIKE ?", ['%'.$this->search.'%']); }); }); } // Status filter if ($this->statusFilter) { if ($this->statusFilter === 'pending_approval') { $query->whereIn('status', ['sent', 'viewed']) ->where('customer_approval_status', 'pending'); } elseif ($this->statusFilter === 'expired') { $query->whereRaw($this->getDateAddExpressionForDatabase('expired')) ->where('status', '!=', 'approved'); } else { $query->where('status', $this->statusFilter); } } // Approval status filter if ($this->approvalStatusFilter) { $query->where('customer_approval_status', $this->approvalStatusFilter); } // Customer filter if ($this->customerFilter) { $query->whereHas('jobCard.customer', function ($customerQuery) { $customerQuery->where('id', $this->customerFilter); }); } // Date filters if ($this->dateFrom) { $query->whereDate('created_at', '>=', $this->dateFrom); } if ($this->dateTo) { $query->whereDate('created_at', '<=', $this->dateTo); } // Advanced filters if ($this->totalAmountMin) { $query->where('total_amount', '>=', $this->totalAmountMin); } if ($this->totalAmountMax) { $query->where('total_amount', '<=', $this->totalAmountMax); } if ($this->validityFilter) { if ($this->validityFilter === 'expired') { $query->whereRaw($this->getDateAddExpressionForDatabase('expired')); } elseif ($this->validityFilter === 'expiring_soon') { $query->whereRaw($this->getDateAddExpressionForDatabase('expiring_soon')); } elseif ($this->validityFilter === 'valid') { $query->whereRaw($this->getDateAddExpressionForDatabase('valid')); } } if ($this->branchFilter) { $query->whereHas('jobCard.branch', function ($branchQuery) { $branchQuery->where('code', $this->branchFilter); }); } // Sorting $query->orderBy($this->sortBy, $this->sortDirection); return $query->paginate($this->perPage); } public function getCustomersProperty() { return Customer::orderBy('first_name')->orderBy('last_name')->get(['id', 'first_name', 'last_name'])->map(function ($customer) { return (object) [ 'id' => $customer->id, 'name' => $customer->name, ]; }); } public function getBranchesProperty() { return \App\Models\Branch::orderBy('name')->get(['code', 'name']); } public function sendEstimate($estimateId) { $estimate = Estimate::findOrFail($estimateId); if (! Auth::user()->can('update', $estimate)) { session()->flash('error', 'You are not authorized to send this estimate.'); return; } if ($estimate->status !== 'draft') { session()->flash('error', 'Only draft estimates can be sent.'); return; } $estimate->update([ 'status' => 'sent', 'sent_to_customer_at' => now(), ]); // TODO: Send email/SMS notification to customer session()->flash('success', 'Estimate sent to customer successfully.'); $this->loadStats(); } public function confirmDelete($estimateId) { $estimate = Estimate::findOrFail($estimateId); if (! Auth::user()->can('delete', $estimate)) { session()->flash('error', 'You are not authorized to delete this estimate.'); return; } // For now, just delete directly. In production, you might want a confirmation modal $estimate->delete(); session()->flash('success', 'Estimate deleted successfully.'); $this->loadStats(); } #[Layout('components.layouts.app')] public function render() { // Get available diagnoses that don't have estimates yet $availableDiagnoses = \App\Models\Diagnosis::whereDoesntHave('estimate') ->with(['jobCard.customer', 'jobCard.vehicle']) ->latest() ->limit(5) ->get(); return view('livewire.estimates.index', [ 'estimates' => $this->getEstimates(), 'customers' => $this->customers, 'branches' => $this->branches, 'stats' => $this->stats, 'availableDiagnoses' => $availableDiagnoses, ]); } }