small ui changes
This commit is contained in:
parent
80b53535c1
commit
2235ab6770
@ -1,5 +1,5 @@
|
|||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
import { auth } from "@/auth"
|
import { auth } from "@/auth.vendor"
|
||||||
import { prisma } from "@/lib/prisma"
|
import { prisma } from "@/lib/prisma"
|
||||||
import { Prisma } from "@prisma/client"
|
import { Prisma } from "@prisma/client"
|
||||||
import { storeSelectPatchStorefrontAi } from "@/lib/prisma/storeSelects"
|
import { storeSelectPatchStorefrontAi } from "@/lib/prisma/storeSelects"
|
||||||
@ -547,7 +547,18 @@ export async function GET(
|
|||||||
return NextResponse.json({ message: "Store ID is required" }, { status: 400 })
|
return NextResponse.json({ message: "Store ID is required" }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = await prisma.store.findFirst({
|
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 },
|
where: { id: storeId, userId: session.user.id },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@ -558,6 +569,21 @@ export async function GET(
|
|||||||
sharedFieldAnswers: 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) {
|
if (!store) {
|
||||||
return NextResponse.json({ message: "Store not found" }, { status: 404 })
|
return NextResponse.json({ message: "Store not found" }, { status: 404 })
|
||||||
@ -568,8 +594,8 @@ export async function GET(
|
|||||||
businessName: store.businessName,
|
businessName: store.businessName,
|
||||||
categoryMetadetailsEnabled: store.categoryMetadetailsEnabled,
|
categoryMetadetailsEnabled: store.categoryMetadetailsEnabled,
|
||||||
aiModules: store.aiModules,
|
aiModules: store.aiModules,
|
||||||
modulePolicies: parseModulePoliciesJson(store.modulePolicies),
|
modulePolicies: parseModulePoliciesJson(store.modulePolicies ?? null),
|
||||||
sharedFieldAnswers: parseSharedFieldAnswersJson(store.sharedFieldAnswers),
|
sharedFieldAnswers: parseSharedFieldAnswersJson(store.sharedFieldAnswers ?? null),
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error reading store settings:", error)
|
console.error("Error reading store settings:", error)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
import { auth } from "@/auth"
|
import { auth } from "@/auth.vendor"
|
||||||
import { prisma } from "@/lib/prisma"
|
import { prisma } from "@/lib/prisma"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
import { auth } from "@/auth"
|
import { auth } from "@/auth.vendor"
|
||||||
import { prisma } from "@/lib/prisma"
|
import { prisma } from "@/lib/prisma"
|
||||||
import { storeSwitchSchema, validationErrorResponse } from "@/lib/api-schemas"
|
import { storeSwitchSchema, validationErrorResponse } from "@/lib/api-schemas"
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,8 @@ import {
|
|||||||
Flag,
|
Flag,
|
||||||
Mail,
|
Mail,
|
||||||
Layers,
|
Layers,
|
||||||
HelpCircle
|
HelpCircle,
|
||||||
|
MessageCircle,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
|
||||||
export default function JunoEngineLayout({
|
export default function JunoEngineLayout({
|
||||||
@ -28,6 +29,11 @@ export default function JunoEngineLayout({
|
|||||||
href: "/dashboard/juno-engine/modules",
|
href: "/dashboard/juno-engine/modules",
|
||||||
icon: Layers
|
icon: Layers
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Storefront Chat",
|
||||||
|
href: "/dashboard/juno-engine/storefront-chat",
|
||||||
|
icon: MessageCircle
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Tickets",
|
label: "Tickets",
|
||||||
href: "/dashboard/juno-engine/tickets",
|
href: "/dashboard/juno-engine/tickets",
|
||||||
|
|||||||
@ -128,8 +128,18 @@ export default function JunoEngineModulesPage() {
|
|||||||
|
|
||||||
const load = useCallback(async (storeId: string) => {
|
const load = useCallback(async (storeId: string) => {
|
||||||
const res = await fetch(`/api/stores/${encodeURIComponent(storeId)}`)
|
const res = await fetch(`/api/stores/${encodeURIComponent(storeId)}`)
|
||||||
if (!res.ok) throw new Error("Failed to load store settings")
|
const raw = await res.json().catch(() => ({}))
|
||||||
const data = (await res.json()) as {
|
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
|
aiModules?: unknown
|
||||||
sharedFieldAnswers?: unknown
|
sharedFieldAnswers?: unknown
|
||||||
categoryMetadetailsEnabled?: boolean
|
categoryMetadetailsEnabled?: boolean
|
||||||
@ -852,7 +862,7 @@ export default function JunoEngineModulesPage() {
|
|||||||
>
|
>
|
||||||
Widget appearance:{" "}
|
Widget appearance:{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/storefront-chat"
|
href="/dashboard/juno-engine/storefront-chat"
|
||||||
className="font-semibold text-indigo-500 hover:underline"
|
className="font-semibold text-indigo-500 hover:underline"
|
||||||
>
|
>
|
||||||
Storefront Chat Inbox
|
Storefront Chat Inbox
|
||||||
|
|||||||
5
src/app/dashboard/juno-engine/storefront-chat/page.tsx
Normal file
5
src/app/dashboard/juno-engine/storefront-chat/page.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import StorefrontChatInbox from "@/components/storefront/StorefrontChatInbox"
|
||||||
|
|
||||||
|
export default function JunoEngineStorefrontChatPage() {
|
||||||
|
return <StorefrontChatInbox />
|
||||||
|
}
|
||||||
@ -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 { redirect } from "next/navigation"
|
||||||
import Link from "next/link"
|
|
||||||
|
|
||||||
export default async function StorefrontChatInboxPage() {
|
/** @deprecated Use JUNO Engine → Storefront Chat */
|
||||||
const session = await auth()
|
export default function StorefrontChatRedirectPage() {
|
||||||
|
redirect("/dashboard/juno-engine/storefront-chat")
|
||||||
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 (
|
|
||||||
<div className="flex flex-col gap-4 p-1">
|
|
||||||
<div className="flex items-start justify-between gap-4">
|
|
||||||
<div className="min-w-0">
|
|
||||||
<h1 className="text-xl font-black text-white">Storefront Chat Inbox</h1>
|
|
||||||
<p className="text-sm text-slate-400 mt-1 truncate">
|
|
||||||
{store.businessName} · {store.shopifyDomain}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="shrink-0 text-right">
|
|
||||||
<p className="text-sm font-black text-white">{conversations.length} visitors</p>
|
|
||||||
<p className="text-xs text-slate-500 mt-1">{sorted.length} messages</p>
|
|
||||||
<p className="text-[11px] text-slate-500 mt-1">
|
|
||||||
{customerCount} customer · {aiCount} AI
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-sm text-slate-400">
|
|
||||||
L1 AI modules and catalog metafields:{" "}
|
|
||||||
<Link
|
|
||||||
href="/dashboard/juno-engine/modules"
|
|
||||||
className="font-semibold text-indigo-400 hover:underline"
|
|
||||||
>
|
|
||||||
JUNO Engine → Modules
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{sorted.length === 0 ? (
|
|
||||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6 text-slate-300">
|
|
||||||
<p className="font-black">No storefront chat messages yet.</p>
|
|
||||||
<p className="text-sm text-slate-400 mt-2">When customers send a message from the widget, it will show here.</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="bg-white/5 border border-white/10 rounded-2xl overflow-hidden">
|
|
||||||
<div className="p-4 border-b border-white/10 flex items-center justify-between">
|
|
||||||
<p className="text-sm font-black text-white">Recent activity</p>
|
|
||||||
<p className="text-xs text-slate-400">Read-only</p>
|
|
||||||
</div>
|
|
||||||
<div className="max-h-[70vh] overflow-y-auto p-4 space-y-3">
|
|
||||||
{sorted.map((m) => (
|
|
||||||
<div key={m.id} className="flex flex-col gap-2">
|
|
||||||
<div className="flex items-center justify-between gap-4">
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
|
||||||
<span
|
|
||||||
className={`text-[10px] font-black uppercase tracking-widest rounded-full px-2 py-1 border ${
|
|
||||||
m.senderType === "AI"
|
|
||||||
? "text-indigo-300 border-indigo-400/30 bg-indigo-500/10"
|
|
||||||
: "text-emerald-300 border-emerald-400/30 bg-emerald-500/10"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{m.senderType === "CUSTOMER" ? "CUSTOMER" : "AI"}
|
|
||||||
</span>
|
|
||||||
<p className="text-xs font-black uppercase tracking-widest text-slate-400 truncate">
|
|
||||||
{m.conversation?.visitorId || "visitor"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{m.conversation?.customerEmail ? (
|
|
||||||
<p className="text-[11px] text-slate-500 truncate">{m.conversation.customerEmail}</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<p className="text-[11px] text-slate-500 shrink-0">
|
|
||||||
{new Date(m.createdAt).toLocaleString()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="whitespace-pre-wrap text-sm text-white/90 bg-black/20 border border-white/10 rounded-2xl px-4 py-3">
|
|
||||||
{m.content}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
Mail,
|
Mail,
|
||||||
Layers,
|
Layers,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
|
MessageCircle,
|
||||||
Store
|
Store
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
@ -77,6 +78,11 @@ const routes = [
|
|||||||
subRoutes: [
|
subRoutes: [
|
||||||
{ label: "Knowledge Base", href: "/dashboard/juno-engine/knowledge-base", icon: HelpCircle },
|
{ label: "Knowledge Base", href: "/dashboard/juno-engine/knowledge-base", icon: HelpCircle },
|
||||||
{ label: "Modules", href: "/dashboard/juno-engine/modules", icon: Layers },
|
{ 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: "Tickets", href: "/dashboard/juno-engine/tickets", icon: Ticket },
|
||||||
{ label: "Flagged Emails", href: "/dashboard/juno-engine/flagged-email", icon: Flag },
|
{ label: "Flagged Emails", href: "/dashboard/juno-engine/flagged-email", icon: Flag },
|
||||||
{ label: "Gmail", href: "/dashboard/juno-engine/gmail-connect", icon: Mail },
|
{ label: "Gmail", href: "/dashboard/juno-engine/gmail-connect", icon: Mail },
|
||||||
|
|||||||
137
src/components/storefront/StorefrontChatInbox.tsx
Normal file
137
src/components/storefront/StorefrontChatInbox.tsx
Normal file
@ -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 (
|
||||||
|
<div className="flex flex-col gap-4 p-1">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h1 className="text-xl font-black text-white">Storefront Chat Inbox</h1>
|
||||||
|
<p className="text-sm text-slate-400 mt-1 truncate">
|
||||||
|
{store.businessName} · {store.shopifyDomain}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="shrink-0 text-right">
|
||||||
|
<p className="text-sm font-black text-white">{conversations.length} visitors</p>
|
||||||
|
<p className="text-xs text-slate-500 mt-1">{sorted.length} messages</p>
|
||||||
|
<p className="text-[11px] text-slate-500 mt-1">
|
||||||
|
{customerCount} customer · {aiCount} AI
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-slate-400">
|
||||||
|
L1 AI modules and catalog metafields:{" "}
|
||||||
|
<Link
|
||||||
|
href="/dashboard/juno-engine/modules"
|
||||||
|
className="font-semibold text-indigo-400 hover:underline"
|
||||||
|
>
|
||||||
|
JUNO Engine → Modules
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{sorted.length === 0 ? (
|
||||||
|
<div className="bg-white/5 border border-white/10 rounded-2xl p-6 text-slate-300">
|
||||||
|
<p className="font-black">No storefront chat messages yet.</p>
|
||||||
|
<p className="text-sm text-slate-400 mt-2">
|
||||||
|
When customers send a message from the widget, it will show here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-white/5 border border-white/10 rounded-2xl overflow-hidden">
|
||||||
|
<div className="p-4 border-b border-white/10 flex items-center justify-between">
|
||||||
|
<p className="text-sm font-black text-white">Recent activity</p>
|
||||||
|
<p className="text-xs text-slate-400">Read-only</p>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-[70vh] overflow-y-auto p-4 space-y-3">
|
||||||
|
{sorted.map((m) => (
|
||||||
|
<div key={m.id} className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<span
|
||||||
|
className={`text-[10px] font-black uppercase tracking-widest rounded-full px-2 py-1 border ${
|
||||||
|
m.senderType === "AI"
|
||||||
|
? "text-indigo-300 border-indigo-400/30 bg-indigo-500/10"
|
||||||
|
: "text-emerald-300 border-emerald-400/30 bg-emerald-500/10"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{m.senderType === "CUSTOMER" ? "CUSTOMER" : "AI"}
|
||||||
|
</span>
|
||||||
|
<p className="text-xs font-black uppercase tracking-widest text-slate-400 truncate">
|
||||||
|
{m.conversation?.visitorId || "visitor"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{m.conversation?.customerEmail ? (
|
||||||
|
<p className="text-[11px] text-slate-500 truncate">
|
||||||
|
{m.conversation.customerEmail}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-slate-500 shrink-0">
|
||||||
|
{new Date(m.createdAt).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="whitespace-pre-wrap text-sm text-white/90 bg-black/20 border border-white/10 rounded-2xl px-4 py-3">
|
||||||
|
{m.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user