Design overhaul, stats page, internal notes
Design: Onest+Unbounded font pairing (Cyrillic-verified, replacing plain
Golos Text), manual light/dark toggle wired to the data-theme cascade
already built in MVP-1, refreshed palette + new chart-mark tokens
(CVD-validated via the dataviz skill's validator), colored avatar
initials, per-channel colored badges, favicon, nicer empty states.
Stats: /stats page — status/channel breakdown, 14-day ticket volume,
avg first-response time, per-agent open-ticket workload. Colors and
chart forms follow the dataviz skill's procedure (categorical order
re-stepped to clear the CVD adjacency check in both themes).
Internal notes: messages.visibility ("public"/"internal") column.
Agent-only notes never reach customer-facing reads (getTicketForCustomer)
or the customer SSE stream (/api/portal/events, reused by the widget) —
fixed at both the initial-fetch and live-delivery layers.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcXH24ky6zjk2UyK5oZUPH
This commit is contained in:
1 parent
4efce5f307
commit
9f1413f0b9
23 files changed
+1315
-64
No files matched your search
@@ -20,5 +20,12 @@ export async function GET(request: Request) {
|
||||
return NextResponse.json({ error: "Invalid token" }, { status: 404 });
|
||||
}
|
||||
|
||||
return createEventStream((event) => ticketEventCustomerId(event) === customer.id);
|
||||
return createEventStream((event) => {
|
||||
if (ticketEventCustomerId(event) !== customer.id) return false;
|
||||
// Internal notes must never reach a customer-facing stream, even though
|
||||
// the initial page-load fetch (getTicketForCustomer) already filters
|
||||
// them out — SSE is a second, independent delivery path for the same data.
|
||||
if (event.type === "message.created" && event.message.visibility === "internal") return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -3,7 +3,7 @@ export const runtime = "nodejs";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { requireSession } from "@/lib/auth/require";
|
||||
import { recordAgentReply, getEmailThreadContext } from "@/lib/tickets/service";
|
||||
import { recordAgentReply, recordInternalNote, getEmailThreadContext } from "@/lib/tickets/service";
|
||||
import { sendTelegramMessage } from "@/lib/telegram/bot";
|
||||
import { sendTicketReplyEmail } from "@/lib/mail/smtp";
|
||||
import { db } from "@/lib/db/client";
|
||||
@@ -12,6 +12,7 @@ import { eq } from "drizzle-orm";
|
||||
|
||||
const messageSchema = z.object({
|
||||
body: z.string().min(1).max(10_000),
|
||||
visibility: z.enum(["public", "internal"]).default("public"),
|
||||
});
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
@@ -30,6 +31,16 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
|
||||
return NextResponse.json({ error: "Ticket not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
if (parsed.data.visibility === "internal") {
|
||||
const result = await recordInternalNote({
|
||||
ticketId: id,
|
||||
agentId: session.user.id,
|
||||
agentName: session.user.name,
|
||||
body: parsed.data.body,
|
||||
});
|
||||
return NextResponse.json(result, { status: 201 });
|
||||
}
|
||||
|
||||
let emailMessageId: string | undefined;
|
||||
if (ticket.channel === "email") {
|
||||
const context = await getEmailThreadContext(id);
|
||||
|
||||
Reference in new issue
Block a user