Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 40 additions & 37 deletions app/admin/[businessId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import {
buildAdminOnboardingConfidence,
canDeleteTestBusiness,
getAdminTestSmsConfidenceState,
isBusinessArchived,
} from '@/lib/admin-dashboard';
import { CopyValueButton } from '@/components/copy-value-button';
Expand All @@ -37,7 +38,6 @@ import {
import { db } from '@/lib/db';
import {
formatDateTime,
formatMessageStatus,
formatRelativeTime,
getLeadCallbackState,
getLeadStatusBadgeVariant,
Expand Down Expand Up @@ -155,6 +155,22 @@ function getConfidenceMilestoneBadgeVariant(variant: 'success' | 'warning' | 'pe
return 'secondary' as const;
}

function getTestSmsConfidenceSummary(state: ReturnType<typeof getAdminTestSmsConfidenceState>) {
if (state === 'delivered') {
return 'Delivered';
}

if (state === 'pending_delivery') {
return 'Pending delivery';
}

if (state === 'failed') {
return 'Failed';
}

return 'Not run';
}

function formatOperatorEventDetailValue(value: unknown): string {
if (value === null || value === undefined) return '-';
if (typeof value === 'string') return value;
Expand Down Expand Up @@ -291,7 +307,7 @@ export default async function AdminBusinessDetailPage({
await requireAdmin();
const activityFilter = getTimelineFilter(searchParams);

const [business, successfulLeadCount, leadCount, callCount, messageCount, recentLeads, recentCalls, recentMessages, recentOwnerNotifications, operatorEvents] =
const [business, successfulLeadCount, leadCount, callCount, messageCount, recentLeads, recentOwnerNotifications, operatorEvents] =
await Promise.all([
db.business.findUnique({
where: { id: params.businessId },
Expand Down Expand Up @@ -325,33 +341,6 @@ export default async function AdminBusinessDetailPage({
lastInteractionAt: true,
},
}),
db.call.findMany({
where: { businessId: params.businessId },
orderBy: { createdAt: 'desc' },
take: 6,
select: {
id: true,
status: true,
missed: true,
answered: true,
dialCallStatus: true,
createdAt: true,
},
}),
db.message.findMany({
where: { businessId: params.businessId, direction: 'OUTBOUND' },
orderBy: { createdAt: 'desc' },
take: 8,
select: {
id: true,
leadId: true,
participant: true,
direction: true,
status: true,
body: true,
createdAt: true,
},
}),
db.ownerNotification.findMany({
where: { businessId: params.businessId },
orderBy: { createdAt: 'desc' },
Expand Down Expand Up @@ -412,6 +401,13 @@ export default async function AdminBusinessDetailPage({
createdAt: event.createdAt,
})),
});
const testSmsConfidenceState = getAdminTestSmsConfidenceState(
operatorEvents.map((event) => ({
type: event.type,
status: event.status,
createdAt: event.createdAt,
}))
);
const timelineFilterCounts = countTimelineFilters(operatorEvents);
const visibleTimelineEvents = operatorEvents.filter((event) => {
return matchesTimelineFilter(event, activityFilter);
Expand Down Expand Up @@ -457,7 +453,7 @@ export default async function AdminBusinessDetailPage({
type: 'webhooks.mismatch_detected',
}
: null);
const latestOutboundSms = recentMessages[0] || null;
const latestTestSmsEvent = operatorEvents.find((event) => event.type.startsWith('admin.test_sms_')) || null;
const latestOwnerAlert = recentOwnerNotifications[0] || null;

const created = getQueryValue(searchParams, 'created') === '1';
Expand Down Expand Up @@ -534,7 +530,11 @@ export default async function AdminBusinessDetailPage({
{provisioned ? <div className="rounded-md border border-accent bg-accent/40 p-3 text-sm">Provisioning finished. Review the health cards below.</div> : null}
{synced ? <div className="rounded-md border border-accent bg-accent/40 p-3 text-sm">Webhook sync complete for {synced.toLowerCase()}.</div> : null}
{statusSaved ? <div className="rounded-md border border-accent bg-accent/40 p-3 text-sm">Business status updated to {statusSaved.replace(/_/g, ' ')}.</div> : null}
{testSms ? <div className="rounded-md border border-accent bg-accent/40 p-3 text-sm">Admin test SMS sent.</div> : null}
{testSms ? (
<div className="rounded-md border border-accent bg-accent/40 p-3 text-sm">
Admin test SMS requested. Watch recent activity for delivery confirmation before you go live.
</div>
) : null}
{archived ? <div className="rounded-md border border-accent bg-accent/40 p-3 text-sm">Business archived safely. Automation is paused.</div> : null}
{restored ? <div className="rounded-md border border-accent bg-accent/40 p-3 text-sm">Business restored and ready for review.</div> : null}
{error ? <div className="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">{error}</div> : null}
Expand Down Expand Up @@ -648,12 +648,13 @@ export default async function AdminBusinessDetailPage({
</div>
)}

<div className="grid gap-3 md:grid-cols-4">
<div className="grid gap-3 md:grid-cols-5">
{[
{ label: 'Leads', value: leadCount },
{ label: 'Calls', value: callCount },
{ label: 'Messages', value: messageCount },
{ label: 'Qualified alerts', value: successfulLeadCount },
{ label: 'Test SMS', value: getTestSmsConfidenceSummary(testSmsConfidenceState) },
].map((item) => (
<div key={item.label} className="rounded-xl border bg-background/80 p-4">
<p className="text-sm text-muted-foreground">{item.label}</p>
Expand Down Expand Up @@ -1156,13 +1157,15 @@ export default async function AdminBusinessDetailPage({
</p>
</div>
<div className="rounded-xl border bg-background/80 p-4">
<p className="font-medium">Latest outbound SMS</p>
<p className="font-medium">Latest test SMS</p>
<p className="mt-2">
{latestOutboundSms
? `${latestOutboundSms.participant === 'OWNER' ? 'Owner SMS' : 'Lead SMS'}${latestOutboundSms.status ? ` · ${formatMessageStatus(latestOutboundSms.status)}` : ''}`
: 'No outbound SMS yet'}
{latestTestSmsEvent?.summary || 'No test SMS recorded'}
</p>
<p className="mt-2 text-xs text-muted-foreground">
{latestTestSmsEvent
? getOperatorEventDetails(latestTestSmsEvent.detailsJson)[0]?.value || 'Open the timeline below for full test delivery detail.'
: 'Run the admin test SMS before you treat onboarding as ready for launch.'}
</p>
<p className="mt-2 text-xs text-muted-foreground">{latestOutboundSms?.body || 'No outbound SMS has been recorded yet.'}</p>
</div>
<div className="rounded-xl border bg-background/80 p-4">
<p className="font-medium">Latest owner alert</p>
Expand Down
1 change: 1 addition & 0 deletions app/admin/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,7 @@ export async function sendBusinessTestSmsAction(formData: FormData) {
toPhone: destinationPhone!,
body: `CallbackCloser admin test: ${business.name} is using ${fromPhone} for live support verification.`,
participant: 'OWNER',
context: 'admin_test',
twilioSubaccountSid: business.twilioSubaccountSid,
messagingServiceSid: business.twilioMessagingServiceSid,
managedTwilioStatus: business.managedTwilioStatus,
Expand Down
35 changes: 12 additions & 23 deletions app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export default async function AdminPage({ searchParams }: { searchParams?: Recor
const view = (getQueryValue(searchParams, 'view') as AdminBoardFilter | null) || 'all';
const adminBusiness = await db.business.findUnique({ where: { ownerClerkId: admin.userId } });

const [businesses, leadCounts, leadActivity, callActivity, messageActivity, notificationFailures, operatorSignals] = await Promise.all([
const [businesses, leadCounts, leadActivity, callActivity, messageActivity, operatorSignals] = await Promise.all([
query
? searchBusinessesForAdmin(query)
: db.business.findMany({
Expand All @@ -108,19 +108,6 @@ export default async function AdminPage({ searchParams }: { searchParams?: Recor
by: ['businessId'],
_max: { createdAt: true },
}),
db.ownerNotification.findMany({
where: {
status: { in: ['FAILED', 'SKIPPED'] },
},
orderBy: { createdAt: 'desc' },
select: {
businessId: true,
status: true,
error: true,
createdAt: true,
},
take: 200,
}),
db.businessOperatorEvent.findMany({
where: {
status: { in: [OperatorEventStatus.FAILED, OperatorEventStatus.WARNING] },
Expand All @@ -129,6 +116,8 @@ export default async function AdminPage({ searchParams }: { searchParams?: Recor
select: {
businessId: true,
status: true,
summary: true,
createdAt: true,
},
take: 600,
}),
Expand All @@ -140,16 +129,16 @@ export default async function AdminPage({ searchParams }: { searchParams?: Recor
);
const callActivityMap = new Map(callActivity.map((item) => [item.businessId, item._max.createdAt]));
const messageActivityMap = new Map(messageActivity.map((item) => [item.businessId, item._max.createdAt]));
const notificationFailureMap = new Map<string, { status: string; error: string | null; createdAt: Date }>();
const latestOperatorSignalMap = new Map<string, { summary: string; createdAt: Date }>();
const operatorSignalMap = new Map<string, { failed: number; warning: number }>();

for (const failure of notificationFailures) {
if (!notificationFailureMap.has(failure.businessId)) {
notificationFailureMap.set(failure.businessId, failure);
}
}

for (const signal of operatorSignals) {
if (!latestOperatorSignalMap.has(signal.businessId)) {
latestOperatorSignalMap.set(signal.businessId, {
summary: signal.summary,
createdAt: signal.createdAt,
});
}
const entry = operatorSignalMap.get(signal.businessId) || { failed: 0, warning: 0 };
if (signal.status === OperatorEventStatus.FAILED) {
entry.failed += 1;
Expand All @@ -174,7 +163,7 @@ export default async function AdminPage({ searchParams }: { searchParams?: Recor
callActivityMap.get(business.id),
messageActivityMap.get(business.id)
);
const latestFailure = notificationFailureMap.get(business.id);
const latestFailure = latestOperatorSignalMap.get(business.id);
const assignedNumber = getManagedTextingNumber(business);
const archived = isBusinessArchived(business);
const paused = !archived && business.provisioningStatus === 'PAUSED';
Expand All @@ -190,7 +179,7 @@ export default async function AdminPage({ searchParams }: { searchParams?: Recor
complianceStarted: managedSummary.complianceStarted,
});
const attentionSignal = latestFailure
? compactCopy(`${latestFailure.error || `${latestFailure.status.toLowerCase()} owner notification`} on ${formatDateTime(latestFailure.createdAt)}.`)
? compactCopy(`${latestFailure.summary} on ${formatDateTime(latestFailure.createdAt)}.`)
: nextStep.tone === 'healthy'
? 'Healthy. No immediate operator action needed.'
: compactCopy(`${nextStep.title}. ${nextStep.detail}`);
Expand Down
Loading
Loading