From 2235ab6770e857c24ceac2ff1cc64aaad443d290 Mon Sep 17 00:00:00 2001 From: Saad Mohsin Date: Sun, 12 Apr 2026 04:21:56 +0500 Subject: [PATCH] small ui changes --- src/app/api/stores/[storeId]/route.ts | 54 +++++-- src/app/api/stores/route.ts | 2 +- src/app/api/stores/switch/route.ts | 2 +- src/app/dashboard/juno-engine/layout.tsx | 8 +- .../dashboard/juno-engine/modules/page.tsx | 16 +- .../juno-engine/storefront-chat/page.tsx | 5 + src/app/dashboard/storefront-chat/page.tsx | 136 +---------------- src/components/Sidebar.tsx | 6 + .../storefront/StorefrontChatInbox.tsx | 137 ++++++++++++++++++ 9 files changed, 213 insertions(+), 153 deletions(-) create mode 100644 src/app/dashboard/juno-engine/storefront-chat/page.tsx create mode 100644 src/components/storefront/StorefrontChatInbox.tsx diff --git a/src/app/api/stores/[storeId]/route.ts b/src/app/api/stores/[storeId]/route.ts index c5932f5..7f8d5aa 100644 --- a/src/app/api/stores/[storeId]/route.ts +++ b/src/app/api/stores/[storeId]/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server" -import { auth } from "@/auth" +import { auth } from "@/auth.vendor" import { prisma } from "@/lib/prisma" import { Prisma } from "@prisma/client" import { storeSelectPatchStorefrontAi } from "@/lib/prisma/storeSelects" @@ -547,17 +547,43 @@ export async function GET( return NextResponse.json({ message: "Store ID is required" }, { status: 400 }) } - const store = await prisma.store.findFirst({ - where: { id: storeId, userId: session.user.id }, - select: { - id: true, - businessName: true, - categoryMetadetailsEnabled: true, - aiModules: true, - modulePolicies: true, - sharedFieldAnswers: true, - }, - }) + type StoreGetRow = { + id: string + businessName: string + categoryMetadetailsEnabled: boolean + aiModules: unknown + modulePolicies?: unknown + sharedFieldAnswers?: unknown + } + + let store: StoreGetRow | null = null + try { + store = await prisma.store.findFirst({ + where: { id: storeId, userId: session.user.id }, + select: { + id: true, + businessName: true, + categoryMetadetailsEnabled: true, + aiModules: true, + modulePolicies: true, + sharedFieldAnswers: true, + }, + }) + } catch (firstErr) { + console.warn( + "[STORE_GET] full select failed (run migrations if modulePolicies/sharedFieldAnswers are missing); retrying minimal", + firstErr, + ) + store = await prisma.store.findFirst({ + where: { id: storeId, userId: session.user.id }, + select: { + id: true, + businessName: true, + categoryMetadetailsEnabled: true, + aiModules: true, + }, + }) + } if (!store) { return NextResponse.json({ message: "Store not found" }, { status: 404 }) @@ -568,8 +594,8 @@ export async function GET( businessName: store.businessName, categoryMetadetailsEnabled: store.categoryMetadetailsEnabled, aiModules: store.aiModules, - modulePolicies: parseModulePoliciesJson(store.modulePolicies), - sharedFieldAnswers: parseSharedFieldAnswersJson(store.sharedFieldAnswers), + modulePolicies: parseModulePoliciesJson(store.modulePolicies ?? null), + sharedFieldAnswers: parseSharedFieldAnswersJson(store.sharedFieldAnswers ?? null), }) } catch (error) { console.error("Error reading store settings:", error) diff --git a/src/app/api/stores/route.ts b/src/app/api/stores/route.ts index cd9af58..b9658b8 100644 --- a/src/app/api/stores/route.ts +++ b/src/app/api/stores/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server" -import { auth } from "@/auth" +import { auth } from "@/auth.vendor" import { prisma } from "@/lib/prisma" /** diff --git a/src/app/api/stores/switch/route.ts b/src/app/api/stores/switch/route.ts index 116d8a7..bd72802 100644 --- a/src/app/api/stores/switch/route.ts +++ b/src/app/api/stores/switch/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server" -import { auth } from "@/auth" +import { auth } from "@/auth.vendor" import { prisma } from "@/lib/prisma" import { storeSwitchSchema, validationErrorResponse } from "@/lib/api-schemas" diff --git a/src/app/dashboard/juno-engine/layout.tsx b/src/app/dashboard/juno-engine/layout.tsx index 64de0e0..424b351 100644 --- a/src/app/dashboard/juno-engine/layout.tsx +++ b/src/app/dashboard/juno-engine/layout.tsx @@ -6,7 +6,8 @@ import { Flag, Mail, Layers, - HelpCircle + HelpCircle, + MessageCircle, } from "lucide-react" export default function JunoEngineLayout({ @@ -28,6 +29,11 @@ export default function JunoEngineLayout({ href: "/dashboard/juno-engine/modules", icon: Layers }, + { + label: "Storefront Chat", + href: "/dashboard/juno-engine/storefront-chat", + icon: MessageCircle + }, { label: "Tickets", href: "/dashboard/juno-engine/tickets", diff --git a/src/app/dashboard/juno-engine/modules/page.tsx b/src/app/dashboard/juno-engine/modules/page.tsx index b26d8ba..73ba249 100644 --- a/src/app/dashboard/juno-engine/modules/page.tsx +++ b/src/app/dashboard/juno-engine/modules/page.tsx @@ -128,8 +128,18 @@ export default function JunoEngineModulesPage() { const load = useCallback(async (storeId: string) => { const res = await fetch(`/api/stores/${encodeURIComponent(storeId)}`) - if (!res.ok) throw new Error("Failed to load store settings") - const data = (await res.json()) as { + const raw = await res.json().catch(() => ({})) + const errBody = raw as { message?: string } + if (!res.ok) { + throw new Error( + typeof errBody.message === "string" && errBody.message.trim() + ? errBody.message + : res.status === 401 + ? "Sign in again to load store settings." + : "Failed to load store settings", + ) + } + const data = raw as { aiModules?: unknown sharedFieldAnswers?: unknown categoryMetadetailsEnabled?: boolean @@ -852,7 +862,7 @@ export default function JunoEngineModulesPage() { > Widget appearance:{" "} Storefront Chat Inbox diff --git a/src/app/dashboard/juno-engine/storefront-chat/page.tsx b/src/app/dashboard/juno-engine/storefront-chat/page.tsx new file mode 100644 index 0000000..a63c95a --- /dev/null +++ b/src/app/dashboard/juno-engine/storefront-chat/page.tsx @@ -0,0 +1,5 @@ +import StorefrontChatInbox from "@/components/storefront/StorefrontChatInbox" + +export default function JunoEngineStorefrontChatPage() { + return +} diff --git a/src/app/dashboard/storefront-chat/page.tsx b/src/app/dashboard/storefront-chat/page.tsx index 7e8b2af..647bb61 100644 --- a/src/app/dashboard/storefront-chat/page.tsx +++ b/src/app/dashboard/storefront-chat/page.tsx @@ -1,136 +1,6 @@ -"use server" - -import { prisma } from "@/lib/prisma" -import { storeSelectDashboardStorefrontInbox } from "@/lib/prisma/storeSelects" -import { auth } from "@/auth.vendor" import { redirect } from "next/navigation" -import Link from "next/link" -export default async function StorefrontChatInboxPage() { - const session = await auth() - - if (!session?.user?.id) { - redirect("/login") - } - - const stores = await prisma.store.findMany({ - where: { userId: session.user.id, onboardingComplete: true }, - select: storeSelectDashboardStorefrontInbox, - }) - - const store = stores.find((s) => s.isActive) ?? stores[0] - - if (!store) { - redirect("/onboarding") - } - - const conversations = await prisma.storefrontChatConversation.findMany({ - where: { storeId: store.id }, - orderBy: { updatedAt: "desc" }, - take: 25, - select: { id: true, visitorId: true, customerEmail: true, updatedAt: true } - }) - - const conversationIds = conversations.map((c: { id: string }) => c.id) - - const messages = await prisma.storefrontChatMessage.findMany({ - where: conversationIds.length ? { conversationId: { in: conversationIds } } : undefined, - orderBy: { createdAt: "desc" }, - take: 200, - select: { - id: true, - content: true, - createdAt: true, - senderType: true, - conversation: { - select: { - visitorId: true, - customerEmail: true - } - } - } - }) - - const sorted = [...messages].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()) - const aiCount = sorted.filter((m) => m.senderType === "AI").length - const customerCount = sorted.filter((m) => m.senderType === "CUSTOMER").length - - return ( -
-
-
-

Storefront Chat Inbox

-

- {store.businessName} · {store.shopifyDomain} -

-
-
-

{conversations.length} visitors

-

{sorted.length} messages

-

- {customerCount} customer · {aiCount} AI -

-
-
- -

- L1 AI modules and catalog metafields:{" "} - - JUNO Engine → Modules - - . -

- - {sorted.length === 0 ? ( -
-

No storefront chat messages yet.

-

When customers send a message from the widget, it will show here.

-
- ) : ( -
-
-

Recent activity

-

Read-only

-
-
- {sorted.map((m) => ( -
-
-
-
- - {m.senderType === "CUSTOMER" ? "CUSTOMER" : "AI"} - -

- {m.conversation?.visitorId || "visitor"} -

-
- {m.conversation?.customerEmail ? ( -

{m.conversation.customerEmail}

- ) : null} -
-

- {new Date(m.createdAt).toLocaleString()} -

-
-
- {m.content} -
-
- ))} -
-
- )} -
- ) +/** @deprecated Use JUNO Engine → Storefront Chat */ +export default function StorefrontChatRedirectPage() { + redirect("/dashboard/juno-engine/storefront-chat") } - diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 9211b32..98b9163 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -16,6 +16,7 @@ import { Mail, Layers, HelpCircle, + MessageCircle, Store } from "lucide-react" import { cn } from "@/lib/utils" @@ -77,6 +78,11 @@ const routes = [ subRoutes: [ { label: "Knowledge Base", href: "/dashboard/juno-engine/knowledge-base", icon: HelpCircle }, { label: "Modules", href: "/dashboard/juno-engine/modules", icon: Layers }, + { + label: "Storefront Chat", + href: "/dashboard/juno-engine/storefront-chat", + icon: MessageCircle, + }, { label: "Tickets", href: "/dashboard/juno-engine/tickets", icon: Ticket }, { label: "Flagged Emails", href: "/dashboard/juno-engine/flagged-email", icon: Flag }, { label: "Gmail", href: "/dashboard/juno-engine/gmail-connect", icon: Mail }, diff --git a/src/components/storefront/StorefrontChatInbox.tsx b/src/components/storefront/StorefrontChatInbox.tsx new file mode 100644 index 0000000..a518317 --- /dev/null +++ b/src/components/storefront/StorefrontChatInbox.tsx @@ -0,0 +1,137 @@ +import { prisma } from "@/lib/prisma" +import { storeSelectDashboardStorefrontInbox } from "@/lib/prisma/storeSelects" +import { auth } from "@/auth.vendor" +import { redirect } from "next/navigation" +import Link from "next/link" + +export default async function StorefrontChatInbox() { + const session = await auth() + + if (!session?.user?.id) { + redirect("/login") + } + + const stores = await prisma.store.findMany({ + where: { userId: session.user.id, onboardingComplete: true }, + select: storeSelectDashboardStorefrontInbox, + }) + + const store = stores.find((s) => s.isActive) ?? stores[0] + + if (!store) { + redirect("/onboarding") + } + + const conversations = await prisma.storefrontChatConversation.findMany({ + where: { storeId: store.id }, + orderBy: { updatedAt: "desc" }, + take: 25, + select: { id: true, visitorId: true, customerEmail: true, updatedAt: true }, + }) + + const conversationIds = conversations.map((c) => c.id) + + const messages = await prisma.storefrontChatMessage.findMany({ + where: conversationIds.length ? { conversationId: { in: conversationIds } } : undefined, + orderBy: { createdAt: "desc" }, + take: 200, + select: { + id: true, + content: true, + createdAt: true, + senderType: true, + conversation: { + select: { + visitorId: true, + customerEmail: true, + }, + }, + }, + }) + + const sorted = [...messages].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()) + const aiCount = sorted.filter((m) => m.senderType === "AI").length + const customerCount = sorted.filter((m) => m.senderType === "CUSTOMER").length + + return ( +
+
+
+

Storefront Chat Inbox

+

+ {store.businessName} · {store.shopifyDomain} +

+
+
+

{conversations.length} visitors

+

{sorted.length} messages

+

+ {customerCount} customer · {aiCount} AI +

+
+
+ +

+ L1 AI modules and catalog metafields:{" "} + + JUNO Engine → Modules + + . +

+ + {sorted.length === 0 ? ( +
+

No storefront chat messages yet.

+

+ When customers send a message from the widget, it will show here. +

+
+ ) : ( +
+
+

Recent activity

+

Read-only

+
+
+ {sorted.map((m) => ( +
+
+
+
+ + {m.senderType === "CUSTOMER" ? "CUSTOMER" : "AI"} + +

+ {m.conversation?.visitorId || "visitor"} +

+
+ {m.conversation?.customerEmail ? ( +

+ {m.conversation.customerEmail} +

+ ) : null} +
+

+ {new Date(m.createdAt).toLocaleString()} +

+
+
+ {m.content} +
+
+ ))} +
+
+ )} +
+ ) +}