Forráskód Böngészése

feat: complete 5 chatbot improvements

1. Admin UI: add ERP Contact ID column to UserManagement with inline
   edit — pencil icon opens input, saves via users.updateErpContactCid
2. Dealer 'my orders' no CID: return helpful LLM note instead of silent
   empty context when erpContactCid is not linked
3. ChatbotWidget.tsx scroll: apply smart scroll (isNearBottomRef) same
   as ChatbotWidgetLive — user can scroll up without snapping back
4. ERP error handling: catch block now sets erpContext to a helpful note
   so LLM tells user lookup is temporarily unavailable
5. Env validation: warn at startup if ERP_API_KEY is unset in production

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tony T 6 napja
szülő
commit
1ba288c808

+ 16 - 2
client/src/components/ChatbotWidget.tsx

@@ -3,7 +3,7 @@
  * Design: Warm Showroom — forest green header, cream body, terracotta accents
  * Floats in bottom-right corner, exactly as it would appear on homelegance.com
  */
-import { useState, useRef, useEffect } from "react";
+import { useState, useRef, useEffect, useCallback } from "react";
 import { MessageCircle, X, Send, Bot, User } from "lucide-react";
 import { motion, AnimatePresence } from "framer-motion";
 
@@ -91,13 +91,23 @@ export default function ChatbotWidget() {
   const [inputValue, setInputValue] = useState("");
   const [isTyping, setIsTyping] = useState(false);
   const messagesEndRef = useRef<HTMLDivElement>(null);
+  const scrollContainerRef = useRef<HTMLDivElement>(null);
+  const isNearBottomRef = useRef(true);
   const inputRef = useRef<HTMLInputElement>(null);
   let nextId = useRef(2);
 
   useEffect(() => {
-    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
+    if (isNearBottomRef.current) {
+      messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
+    }
   }, [messages, isTyping]);
 
+  const handleScroll = useCallback(() => {
+    const el = scrollContainerRef.current;
+    if (!el) return;
+    isNearBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 100;
+  }, []);
+
   useEffect(() => {
     if (isOpen) {
       setTimeout(() => inputRef.current?.focus(), 300);
@@ -121,6 +131,8 @@ export default function ChatbotWidget() {
     const messageText = text || inputValue.trim();
     if (!messageText) return;
 
+    isNearBottomRef.current = true;
+
     const userMsg: Message = {
       id: nextId.current++,
       text: messageText,
@@ -208,6 +220,8 @@ export default function ChatbotWidget() {
 
             {/* Messages area */}
             <div
+              ref={scrollContainerRef}
+              onScroll={handleScroll}
               className="flex-1 overflow-y-auto px-4 py-4 space-y-4"
               style={{ background: "#FFFBEB" }}
             >

+ 68 - 3
client/src/pages/UserManagement.tsx

@@ -30,7 +30,7 @@ import {
   CheckCircle2, AlertTriangle, Crown, Mail, Send,
   Clock, XCircle, RefreshCw, Download, History,
   MoreHorizontal, UserPlus, CheckSquare, Square,
-  ArrowUpDown, Eye, Copy, ExternalLink,
+  ArrowUpDown, Eye, Copy, ExternalLink, Pencil, Link2Off,
 } from "lucide-react";
 
 /* ─── Role config ─── */
@@ -286,6 +286,9 @@ function UsersTab() {
     open: boolean; type: "role" | "delete" | "bulkRole" | "bulkDelete";
     userId?: number; userName?: string; currentRole?: string; newRole?: string;
   }>({ open: false, type: "role" });
+  const [erpEditState, setErpEditState] = useState<{
+    userId: number | null; value: string;
+  }>({ userId: null, value: "" });
 
   const utils = trpc.useUtils();
   const { data: usersList, isLoading } = trpc.users.list.useQuery();
@@ -334,6 +337,15 @@ function UsersTab() {
     onError: (error) => toast.error("Bulk delete failed", { description: error.message }),
   });
 
+  const updateErpCidMutation = trpc.users.updateErpContactCid.useMutation({
+    onSuccess: () => {
+      toast.success("ERP Contact ID updated");
+      utils.users.list.invalidate();
+      setErpEditState({ userId: null, value: "" });
+    },
+    onError: (error) => toast.error("Failed to update ERP Contact ID", { description: error.message }),
+  });
+
   const exportCsvQuery = trpc.users.exportCsv.useQuery(undefined, { enabled: false });
 
   const filteredUsers = useMemo(() => {
@@ -472,6 +484,7 @@ function UsersTab() {
                 </th>
                 <th className="text-left text-xs font-semibold px-4 py-3" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>User</th>
                 <th className="text-left text-xs font-semibold px-4 py-3" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Role</th>
+                <th className="text-left text-xs font-semibold px-4 py-3 hidden lg:table-cell" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>ERP Contact ID</th>
                 <th className="text-left text-xs font-semibold px-4 py-3 hidden md:table-cell" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Joined</th>
                 <th className="text-left text-xs font-semibold px-4 py-3 hidden md:table-cell" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Last Active</th>
                 <th className="text-right text-xs font-semibold px-4 py-3" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Actions</th>
@@ -480,14 +493,14 @@ function UsersTab() {
             <tbody>
               {isLoading ? (
                 <tr>
-                  <td colSpan={6} className="text-center py-12">
+                  <td colSpan={7} className="text-center py-12">
                     <div className="animate-spin w-6 h-6 border-2 rounded-full mx-auto" style={{ borderColor: "#14532D", borderTopColor: "transparent" }} />
                     <p className="text-sm mt-2" style={{ color: "#a8a29e" }}>Loading users...</p>
                   </td>
                 </tr>
               ) : filteredUsers.length === 0 ? (
                 <tr>
-                  <td colSpan={6} className="text-center py-12">
+                  <td colSpan={7} className="text-center py-12">
                     <Users className="w-8 h-8 mx-auto mb-2" style={{ color: "#d6d3d1" }} />
                     <p className="text-sm" style={{ color: "#a8a29e" }}>No users found</p>
                   </td>
@@ -533,6 +546,58 @@ function UsersTab() {
                         </div>
                       </td>
                       <td className="px-4 py-3"><RoleBadge role={u.role} /></td>
+                      <td className="px-4 py-3 hidden lg:table-cell">
+                        {erpEditState.userId === u.id ? (
+                          <div className="flex items-center gap-1">
+                            <input
+                              type="text"
+                              value={erpEditState.value}
+                              onChange={(e) => setErpEditState(s => ({ ...s, value: e.target.value }))}
+                              placeholder="e.g. C001234"
+                              className="px-2 py-1 text-xs rounded border w-28"
+                              style={{ borderColor: "#e7e0d5", fontFamily: "'Source Sans 3', sans-serif" }}
+                              onKeyDown={(e) => {
+                                if (e.key === "Enter") {
+                                  updateErpCidMutation.mutate({ userId: u.id, erpContactCid: erpEditState.value.trim() || null });
+                                } else if (e.key === "Escape") {
+                                  setErpEditState({ userId: null, value: "" });
+                                }
+                              }}
+                              autoFocus
+                            />
+                            <button
+                              onClick={() => updateErpCidMutation.mutate({ userId: u.id, erpContactCid: erpEditState.value.trim() || null })}
+                              disabled={updateErpCidMutation.isPending}
+                              className="text-xs px-2 py-1 rounded text-white"
+                              style={{ background: "#14532D" }}
+                            >
+                              Save
+                            </button>
+                            <button
+                              onClick={() => setErpEditState({ userId: null, value: "" })}
+                              className="text-xs px-2 py-1 rounded border"
+                              style={{ borderColor: "#e7e0d5" }}
+                            >
+                              Cancel
+                            </button>
+                          </div>
+                        ) : (
+                          <div className="flex items-center gap-1.5 group">
+                            <span className="text-xs font-mono" style={{ color: u.erpContactCid ? "#292524" : "#a8a29e" }}>
+                              {u.erpContactCid || "—"}
+                            </span>
+                            {!isSelf && (
+                              <button
+                                onClick={() => setErpEditState({ userId: u.id, value: u.erpContactCid ?? "" })}
+                                className="opacity-0 group-hover:opacity-100 transition-opacity"
+                                title="Edit ERP Contact ID"
+                              >
+                                <Pencil className="w-3 h-3" style={{ color: "#78716C" }} />
+                              </button>
+                            )}
+                          </div>
+                        )}
+                      </td>
                       <td className="px-4 py-3 hidden md:table-cell">
                         <span className="text-xs" style={{ color: "#78716C" }}>
                           {u.createdAt ? new Date(u.createdAt).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }) : "—"}

+ 4 - 0
server/_core/env.ts

@@ -10,3 +10,7 @@ export const ENV = {
   erpApiKey: process.env.ERP_API_KEY ?? "",
   dealerPortalSsoSecret: process.env.DEALER_PORTAL_SSO_SECRET ?? "",
 };
+
+if (process.env.NODE_ENV === "production" && !process.env.ERP_API_KEY) {
+  console.warn("[ENV] WARNING: ERP_API_KEY is not set — ERP features will be disabled");
+}

+ 3 - 1
server/routers.ts

@@ -365,6 +365,8 @@ export const appRouter = router({
               const cid = ctx.user?.erpContactCid ?? (conversation as any).customerId as string | undefined;
               if (cid) {
                 erpContext = await lookupOrdersByCustomer(cid, 5, userCtx);
+              } else {
+                erpContext = "NOTE: This user has not been linked to an ERP dealer account. Their erpContactCid is not set. Let them know they should contact support to link their dealer account before order history can be retrieved.";
               }
 
             // 3. PO number lookup  —  "PO-12345", "purchase order 5678"
@@ -407,8 +409,8 @@ export const appRouter = router({
               }
             }
           } catch (erpErr) {
-            // ERP errors must never break the chat — just log and continue without context
             console.error("[ERP] intent lookup error:", erpErr);
+            erpContext = "NOTE: ERP data lookup failed (service temporarily unavailable). Do not mention specific order details. Let the user know order lookup is temporarily unavailable and suggest they try again shortly or contact their sales rep.";
           }
         }