'user'|'assistant', 'content' => '…'] * * The full history is passed to GeminiChatbotService on every turn so * Gemini can resolve follow-up messages (e.g. answering a clarification * question) in context. */ public array $chatHistory = []; // ───────────────────────────────────────────────────────────────────────── public function mount(): void { $this->plants = DB::table('plants') ->whereNull('deleted_at') ->orderBy('name') ->get(['id', 'name']) ->toArray(); $this->dateFrom = now()->startOfMonth()->format('Y-m-d'); $this->dateTo = now()->format('Y-m-d'); } // ── Mode switching ──────────────────────────────────────────────────────── public function setMode(string $mode): void { $this->mode = $mode; } public function setReportType(string $type): void { $this->reportType = $type; // Clear previous results when switching report type $this->result = ''; $this->hasResult = false; $this->invoiceResult = ''; $this->hasInvoiceResult = false; $this->invoiceStatusResult = ''; $this->hasInvoiceStatusResult = false; } // ── Basic mode — Production helpers ────────────────────────────────────── public function updatedSelectedPlantId(): void { $this->selectedLineId = null; $this->lines = []; $this->result = ''; $this->hasResult = false; if ($this->selectedPlantId) { $this->lines = DB::table('lines') ->whereNull('deleted_at') ->where('plant_id', $this->selectedPlantId) ->orderBy('name') ->get(['id', 'name']) ->toArray(); } } public function updatedSelectedLineId(): void { $this->result = ''; $this->hasResult = false; } public function fetchProduction(): void { if (! $this->selectedPlantId) { $this->result = 'Please select a plant.'; $this->hasResult = true; return; } $query = DB::table('production_quantities') ->whereNull('deleted_at') ->where('plant_id', $this->selectedPlantId) ->whereDate('created_at', '>=', $this->dateFrom) ->whereDate('created_at', '<=', $this->dateTo); if ($this->selectedLineId) { $query->where('line_id', $this->selectedLineId); } $count = $query->count(); $plantName = collect($this->plants) ->firstWhere('id', $this->selectedPlantId)?->name ?? 'Unknown Plant'; $lineName = $this->selectedLineId ? (collect($this->lines)->firstWhere('id', $this->selectedLineId)?->name ?? 'Unknown Line') : 'All Lines'; $from = \Carbon\Carbon::parse($this->dateFrom)->format('d M Y'); $to = \Carbon\Carbon::parse($this->dateTo)->format('d M Y'); $this->result = "Production count for {$plantName} / {$lineName} from {$from} to {$to}: {$count} records."; $this->hasResult = true; } // ── Basic mode — Invoice report (type lookup) ───────────────────────────── public function updatedInvoicePlantId(): void { $this->invoiceResult = ''; $this->hasInvoiceResult = false; } public function updatedInvoiceItemCode(): void { $this->invoiceResult = ''; $this->hasInvoiceResult = false; } public function fetchInvoiceReport(): void { if (! $this->invoicePlantId) { $this->invoiceResult = 'Please select a plant.'; $this->hasInvoiceResult = true; return; } $itemCode = trim($this->invoiceItemCode); if ($itemCode === '') { $this->invoiceResult = 'Please enter an item code.'; $this->hasInvoiceResult = true; return; } $plantName = collect($this->plants) ->firstWhere('id', $this->invoicePlantId)?->name ?? 'Unknown Plant'; try { $rows = DB::select(" WITH plant_item AS ( SELECT ? AS user_plant, ? AS user_item_code ), t1 AS ( SELECT plants.id AS plant_id, plants.name AS plant_name, ARRAY_AGG(items.code) AS item_codes FROM plants LEFT JOIN items ON plants.id = items.plant_id GROUP BY plants.id, plants.name ), t2 AS ( SELECT t1.plant_id, t1.plant_name, CASE WHEN plant_item.user_item_code = ANY(t1.item_codes) THEN 1 ELSE 0 END AS exists_flag FROM t1 CROSS JOIN plant_item WHERE t1.plant_name = plant_item.user_plant ), t3 AS ( SELECT t2.plant_id, t2.plant_name, t2.exists_flag, plant_item.user_item_code FROM t2 LEFT JOIN plant_item ON plant_item.user_plant = t2.plant_name ), t4 AS ( SELECT items.id AS item_id, t3.plant_id, t3.plant_name, t3.exists_flag, t3.user_item_code FROM t3 LEFT JOIN items ON t3.plant_id = items.plant_id AND t3.user_item_code = items.code ) SELECT t4.item_id, t4.plant_id, t4.plant_name, t4.exists_flag, t4.user_item_code, COALESCE(sticker_masters.material_type, 0) AS material_type, CASE WHEN sticker_masters.item_id IS NULL THEN 'no match found' WHEN COALESCE(sticker_masters.material_type, 0) = 0 THEN 'serial invoice' ELSE 'material invoice' END AS invoice_description FROM t4 LEFT JOIN sticker_masters ON sticker_masters.plant_id = t4.plant_id AND sticker_masters.item_id = t4.item_id ", [$plantName, $itemCode]); } catch (\Exception $e) { Log::error('ChatBot: invoice report query failed', [ 'plant' => $plantName, 'item_code' => $itemCode, 'error' => $e->getMessage(), ]); $this->invoiceResult = "Sorry, I couldn't fetch data. Please try again or contact support."; $this->hasInvoiceResult = true; return; } if (empty($rows)) { $this->invoiceResult = "No data found for plant \"{$plantName}\". Please verify the plant selection."; $this->hasInvoiceResult = true; return; } $row = $rows[0]; if ((int) $row->exists_flag === 0) { $this->invoiceResult = 'Provided item code does not exist in the item table.'; } else { switch ($row->invoice_description) { case 'no match found': $this->invoiceResult = "Item not found in sticker master for the plant {$row->plant_name}."; break; case 'serial invoice': $this->invoiceResult = 'It is a serial invoice item.'; break; case 'material invoice': $this->invoiceResult = 'It is a material invoice item.'; break; default: $this->invoiceResult = 'Unexpected result. Please contact support.'; } } $this->hasInvoiceResult = true; } // ── Basic mode — Invoice status (scan status) ───────────────────────────── public function updatedInvoiceNumber(): void { $this->invoiceStatusResult = ''; $this->hasInvoiceStatusResult = false; } /** * Looks up how many serials within an invoice have been scanned / not scanned. * Delegates to ChatbotService so the DB query and formatting logic live in one place. */ public function fetchInvoiceStatus(): void { $invoiceNumber = trim(preg_replace('/\s+/', '', $this->invoiceNumber)); if (empty($invoiceNumber)) { $this->invoiceStatusResult = 'Please enter a valid invoice number.'; $this->hasInvoiceStatusResult = true; return; } try { /** @var ChatbotService $service */ $service = app(ChatbotService::class); $this->invoiceStatusResult = $service->ask("invoice = {$invoiceNumber}"); } catch (\Throwable $e) { Log::error('ChatBot: invoice status fetch failed', [ 'invoice' => $invoiceNumber, 'error' => $e->getMessage(), ]); $this->invoiceStatusResult = "Sorry, I couldn't fetch data for invoice {$invoiceNumber}. " . 'Please try again or contact support.'; } $this->hasInvoiceStatusResult = true; } // ── Advanced mode (Gemini-powered) ──────────────────────────────────────── /** * Handles a free-text user message in advanced mode. * * Steps: * 1. Appends the user message to chatHistory immediately (UI feedback). * 2. Calls GeminiChatbotService with the full prior history for context. * 3. Gemini classifies the intent, extracts params, and either: * a) runs the appropriate DB query and returns the result, or * b) returns a clarification question if intent is ambiguous. * 4. Appends the assistant reply to chatHistory. */ public function askAdvanced(): void { $question = trim($this->advancedQuestion); if (empty($question)) { return; } // Show the user's message in the chat bubble immediately $this->chatHistory[] = [ 'role' => 'user', 'content' => $question, ]; $this->advancedQuestion = ''; $this->isAdvancedLoading = true; try { /** @var GeminiChatbotService $gemini */ $gemini = app(GeminiChatbotService::class); // Pass history *without* the turn we just appended — the new user // message is passed separately so Gemini sees it as the latest turn. $priorHistory = array_slice($this->chatHistory, 0, -1); $answer = $gemini->processMessage($priorHistory, $question); } catch (\Throwable $e) { Log::error('ChatBot: advanced ask failed', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); $answer = 'Sorry, something went wrong. Please try again.'; } $this->chatHistory[] = [ 'role' => 'assistant', 'content' => $answer, ]; $this->isAdvancedLoading = false; } public function clearAdvancedChat(): void { $this->chatHistory = []; $this->advancedQuestion = ''; $this->isAdvancedLoading = false; } // ── Panel controls ──────────────────────────────────────────────────────── public function toggleChat(): void { $this->isOpen = ! $this->isOpen; } public function resetForm(): void { // Basic mode — shared $this->reportType = ''; // Basic mode — production $this->selectedPlantId = null; $this->selectedLineId = null; $this->lines = []; $this->result = ''; $this->hasResult = false; $this->dateFrom = now()->startOfMonth()->format('Y-m-d'); $this->dateTo = now()->format('Y-m-d'); // Basic mode — invoice type lookup $this->invoicePlantId = null; $this->invoiceItemCode = ''; $this->invoiceResult = ''; $this->hasInvoiceResult = false; // Basic mode — invoice scan status $this->invoiceNumber = ''; $this->invoiceStatusResult = ''; $this->hasInvoiceStatusResult = false; // Advanced mode $this->clearAdvancedChat(); // Go back to mode selector $this->mode = 'select'; } public function render() { return view('livewire.chat-bot'); } }