diff --git a/app/Livewire/ChatBot.php b/app/Livewire/ChatBot.php index 34d909b..9a3261e 100644 --- a/app/Livewire/ChatBot.php +++ b/app/Livewire/ChatBot.php @@ -51,9 +51,18 @@ class ChatBot extends Component // ── Basic mode — Invoice status (scan status) ───────────────────────────── public string $invoiceNumber = ''; - public string $invoiceStatusResult = ''; + public string $invoiceStatusResult = ''; // kept for simple error strings public bool $hasInvoiceStatusResult = false; + /** + * Structured result from ChatbotService::getInvoiceData(). + * Shape: type, message, invoice_number, total, scanned, not_scanned, unscanned_serials[] + */ + public array $invoiceStatusData = []; + + /** Controls whether all unscanned serials are shown (vs the first 10). */ + public bool $showAllUnscanned = false; + // ── Advanced mode ───────────────────────────────────────────────────────── public string $advancedQuestion = ''; public string $advancedResult = ''; @@ -102,6 +111,8 @@ class ChatBot extends Component $this->hasInvoiceResult = false; $this->invoiceStatusResult = ''; $this->hasInvoiceStatusResult = false; + $this->invoiceStatusData = []; + $this->showAllUnscanned = false; } // ── Basic mode — Production helpers ────────────────────────────────────── @@ -304,11 +315,14 @@ class ChatBot extends Component { $this->invoiceStatusResult = ''; $this->hasInvoiceStatusResult = false; + $this->invoiceStatusData = []; + $this->showAllUnscanned = 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. + * Stores structured data in $invoiceStatusData so the blade can render + * a "show more" serial-number list without dumping 70+ serials in one blob. */ public function fetchInvoiceStatus(): void { @@ -316,26 +330,48 @@ class ChatBot extends Component if (empty($invoiceNumber)) { $this->invoiceStatusResult = 'Please enter a valid invoice number.'; + $this->invoiceStatusData = []; + $this->showAllUnscanned = false; $this->hasInvoiceStatusResult = true; return; } try { - /** @var ChatbotService $service */ - $service = app(ChatbotService::class); - $this->invoiceStatusResult = $service->ask("invoice = {$invoiceNumber}"); + /** @var \App\Services\ChatbotService $service */ + $service = app(\App\Services\ChatbotService::class); + $data = $service->getInvoiceData($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.'; + $data = [ + 'type' => 'error', + 'message' => "Sorry, I couldn't fetch data for invoice {$invoiceNumber}. " + . 'Please try again or contact support.', + 'invoice_number' => $invoiceNumber, + 'total' => 0, + 'scanned' => 0, + 'not_scanned' => 0, + 'unscanned_serials' => [], + ]; } + $this->invoiceStatusData = $data; + $this->invoiceStatusResult = $data['message']; // fallback plain-text copy + $this->showAllUnscanned = false; $this->hasInvoiceStatusResult = true; } + /** + * Toggles the "show all / show less" state for unscanned serial numbers + * in the Basic → Invoice Status result card. + */ + public function toggleShowAllUnscanned(): void + { + $this->showAllUnscanned = ! $this->showAllUnscanned; + } + // ── Advanced mode (Gemini-powered) ──────────────────────────────────────── /** @@ -429,6 +465,8 @@ class ChatBot extends Component $this->invoiceNumber = ''; $this->invoiceStatusResult = ''; $this->hasInvoiceStatusResult = false; + $this->invoiceStatusData = []; + $this->showAllUnscanned = false; // Advanced mode $this->clearAdvancedChat(); diff --git a/app/Services/ChatbotService.php b/app/Services/ChatbotService.php index 9e03a4d..e35b043 100644 --- a/app/Services/ChatbotService.php +++ b/app/Services/ChatbotService.php @@ -83,14 +83,87 @@ class ChatbotService /** * Looks up scan status for an invoice number in invoice_validations. + * Returns a plain-English string (used by the Advanced / free-text path). + * Structured callers should use getInvoiceData() directly. */ private function handleInvoice(string $invoiceNumber, string $_unused = ''): string { - // Strip any whitespace within the invoice number itself + $data = $this->getInvoiceData($invoiceNumber); + + // For the plain-text path (advanced mode / ChatbotService::ask()), + // reassemble a human-readable sentence from the structured data. + if (in_array($data['type'], ['invalid', 'error', 'not_found'], true)) { + return $data['message']; + } + + if ($data['type'] === 'all_scanned') { + $n = $data['total']; + $itemWord = $n === 1 ? 'serial number' : 'serial numbers'; + return "For invoice number {$data['invoice_number']}, all {$n} {$itemWord} " + . ($n === 1 ? 'has' : 'have') . ' been scanned. ✅'; + } + + // partial or none_scanned + $total = $data['total']; + $scanned = $data['scanned']; + $notScan = $data['not_scanned']; + $inv = $data['invoice_number']; + $itemWord = $total === 1 ? 'serial number' : 'serial numbers'; + + if ($scanned === 0) { + $msg = "For invoice number {$inv}, there " + . ($total === 1 ? 'is' : 'are') . " {$total} {$itemWord} " + . 'and none have been scanned.'; + } else { + $msg = "For invoice number {$inv}, there " + . ($total === 1 ? 'is' : 'are') . " {$total} {$itemWord} in total. " + . "Out of which {$scanned} " + . ($scanned === 1 ? 'has' : 'have') . ' been scanned and ' + . "{$notScan} " + . ($notScan === 1 ? 'has' : 'have') . ' not been scanned.'; + } + + if (! empty($data['unscanned_serials'])) { + $msg .= ' Unscanned serial numbers are: ' + . implode(', ', $data['unscanned_serials']) . '.'; + } + + return $msg; + } + + // ───────────────────────────────────────────────────────────────────────── + // Public structured accessor — used by ChatBot (Basic mode) + // ───────────────────────────────────────────────────────────────────────── + + /** + * Returns structured scan-status data for an invoice number. + * + * Return shape: + * [ + * 'type' => 'all_scanned' | 'partial' | 'none_scanned' + * | 'not_found' | 'error' | 'invalid', + * 'message' => string, // one-line human summary (no serial list) + * 'invoice_number' => string, + * 'total' => int, + * 'scanned' => int, + * 'not_scanned' => int, + * 'unscanned_serials' => string[], // full list — may be large + * ] + */ + public function getInvoiceData(string $invoiceNumber): array + { $invoiceNumber = preg_replace('/\s+/', '', $invoiceNumber); if (empty($invoiceNumber)) { - return 'Please provide a valid invoice number. Example: invoice = 3RA0013333'; + return [ + 'type' => 'invalid', + 'message' => 'Please provide a valid invoice number. Example: invoice = 3RA0013333', + 'invoice_number' => '', + 'total' => 0, + 'scanned' => 0, + 'not_scanned' => 0, + 'unscanned_serials' => [], + ]; } try { @@ -114,71 +187,88 @@ class ChatbotService 'error' => $e->getMessage(), ]); - return "Sorry, I couldn't fetch data for invoice {$invoiceNumber}. " - . 'Please try again or contact support if this keeps happening.'; + return [ + 'type' => 'error', + 'message' => "Sorry, I couldn't fetch data for invoice {$invoiceNumber}. " + . 'Please try again or contact support if this keeps happening.', + 'invoice_number' => $invoiceNumber, + 'total' => 0, + 'scanned' => 0, + 'not_scanned' => 0, + 'unscanned_serials' => [], + ]; } if (empty($rows)) { - return "No records found for invoice number {$invoiceNumber}. " - . 'Please double-check the invoice number and try again.'; + return [ + 'type' => 'not_found', + 'message' => "No records found for invoice number {$invoiceNumber}. " + . 'Please double-check the invoice number and try again.', + 'invoice_number' => $invoiceNumber, + 'total' => 0, + 'scanned' => 0, + 'not_scanned' => 0, + 'unscanned_serials' => [], + ]; } - return $this->formatInvoiceResponse($invoiceNumber, $rows); - } - - /** - * Turn the raw DB rows into a plain-English summary. - */ - private function formatInvoiceResponse(string $invoiceNumber, array $rows): string - { + // ── Aggregate rows ──────────────────────────────────────────────────── $totalScanned = 0; $totalNotScanned = 0; - $unscannedList = null; + $unscannedSerials = []; foreach ($rows as $row) { if ($row->status === 'not scanned') { $totalNotScanned = (int) $row->total_count; - $unscannedList = $row->serial_numbers_not_scanned; + if (! empty($row->serial_numbers_not_scanned)) { + $unscannedSerials = array_values( + array_filter( + array_map('trim', explode(',', $row->serial_numbers_not_scanned)) + ) + ); + } } else { $totalScanned += (int) $row->total_count; } } $grandTotal = $totalScanned + $totalNotScanned; - $itemWord = $grandTotal === 1 ? 'serial number' : 'serial numbers'; // ── All scanned ─────────────────────────────────────────────────────── if ($totalNotScanned === 0) { - return "For invoice number {$invoiceNumber}, all {$grandTotal} {$itemWord} " - . ($grandTotal === 1 ? 'has' : 'have') . ' been scanned. ✅'; + $n = $grandTotal; + $itemWord = $n === 1 ? 'serial number' : 'serial numbers'; + return [ + 'type' => 'all_scanned', + 'message' => "All {$n} {$itemWord} scanned for invoice {$invoiceNumber}. ✅", + 'invoice_number' => $invoiceNumber, + 'total' => $grandTotal, + 'scanned' => $totalScanned, + 'not_scanned' => 0, + 'unscanned_serials' => [], + ]; } - // ── None scanned ────────────────────────────────────────────────────── + // ── None / partial scanned ──────────────────────────────────────────── + $type = $totalScanned === 0 ? 'none_scanned' : 'partial'; + $itemWord = $grandTotal === 1 ? 'serial number' : 'serial numbers'; + if ($totalScanned === 0) { - $msg = "For invoice number {$invoiceNumber}, there " - . ($grandTotal === 1 ? 'is' : 'are') . " {$grandTotal} {$itemWord} " - . 'and none have been scanned.'; - - if ($unscannedList) { - $msg .= " Unscanned serial numbers are: {$unscannedList}."; - } - - return $msg; + $summary = "Invoice {$invoiceNumber} — {$grandTotal} {$itemWord}, none scanned yet."; + } else { + $summary = "Invoice {$invoiceNumber} — {$grandTotal} {$itemWord} total: " + . "{$totalScanned} scanned, {$totalNotScanned} not scanned."; } - // ── Mixed ───────────────────────────────────────────────────────────── - $msg = "For invoice number {$invoiceNumber}, there " - . ($grandTotal === 1 ? 'is' : 'are') . " {$grandTotal} {$itemWord} in total. " - . "Out of which {$totalScanned} " - . ($totalScanned === 1 ? 'has' : 'have') . ' been scanned and ' - . "{$totalNotScanned} " - . ($totalNotScanned === 1 ? 'has' : 'have') . ' not been scanned.'; - - if ($unscannedList) { - $msg .= " Unscanned serial numbers are: {$unscannedList}."; - } - - return $msg; + return [ + 'type' => $type, + 'message' => $summary, + 'invoice_number' => $invoiceNumber, + 'total' => $grandTotal, + 'scanned' => $totalScanned, + 'not_scanned' => $totalNotScanned, + 'unscanned_serials' => $unscannedSerials, + ]; } // ───────────────────────────────────────────────────────────────────────── diff --git a/resources/views/livewire/chat-bot.blade.php b/resources/views/livewire/chat-bot.blade.php index 303d91c..8bc9014 100644 --- a/resources/views/livewire/chat-bot.blade.php +++ b/resources/views/livewire/chat-bot.blade.php @@ -174,7 +174,7 @@ {{-- ── BASIC MODE ── --}} {{-- ══════════════════════════════════════════════════════════════════ --}} @if($mode === 'basic') -
+ {{ $sd['message'] ?? $invoiceStatusResult }} +
+ + {{-- ── Count pills (only for real invoice data) ── --}} + @if($sdTotal > 0) ++ Unscanned serial numbers + + {{ $sdCount }} + +
+ + {{-- Serial chips — first 10, or all when expanded --}} + @php + $visibleSerials = $showAllUnscanned + ? $sdSerials + : array_slice($sdSerials, 0, 10); + $hiddenCount = $sdCount - 10; + @endphp + +{{ $invoiceStatusResult }}
+