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
14 changes: 7 additions & 7 deletions app/(auth)/sign-up/[[...sign-up]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@ import { resolveSignedInAppDestination } from '@/lib/public-auth-routing';
function getIntentCopy(intent: string | undefined) {
if (intent === 'pilot') {
return {
label: 'Start Free Pilot',
title: 'Create your account and start pilot onboarding',
label: 'Start 14-day pilot',
title: 'Create your account and start your 14-day pilot',
detail:
'This path is for a business owner creating a new CallbackCloser account. If you are already signed in, CallbackCloser will send you to onboarding, your dashboard, or the admin new-business flow based on your role.',
'Create your CallbackCloser account here. Once you are in, we create your workspace, handle the setup for you, and notify you when your Lead Recovery Command Center is ready.',
};
}

return {
label: 'Create Account',
title: 'Create your CallbackCloser account',
detail:
'Create a new owner account here. Founder-operated customer pilot setup is separate and stays inside the admin new-business flow.',
detail:
'Create a new owner account here. CallbackCloser handles setup for you, then unlocks your workspace when everything is ready.',
};
}

Expand All @@ -46,7 +46,7 @@ export default async function SignUpPage({
<h1 className="text-3xl font-semibold tracking-tight">{copy.title}</h1>
<p className="text-muted-foreground">{copy.detail}</p>
<p className="text-sm text-muted-foreground">
Existing users should sign in. CallbackCloser operators setting up a customer pilot should use the admin new-business flow, not public signup.
Existing users should sign in. We keep customer setup managed, so you do not need to configure the phone or messaging system yourself.
</p>
</section>
<div className="flex justify-center lg:justify-end">
Expand Down Expand Up @@ -86,7 +86,7 @@ export default async function SignUpPage({
<h1 className="text-3xl font-semibold tracking-tight">{copy.title}</h1>
<p className="text-muted-foreground">{copy.detail}</p>
<p className="text-sm text-muted-foreground">
Existing users should sign in. CallbackCloser operators setting up a customer pilot should use the admin new-business flow, not public signup.
Existing users should sign in. We keep customer setup managed, so you do not need to configure the phone or messaging system yourself.
</p>
</section>
<div className="flex justify-center lg:justify-end">
Expand Down
16 changes: 16 additions & 0 deletions app/admin/[businessId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { Select } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { buildAdminCustomerOpenHref } from '@/lib/admin-customer-paths';
import { buildAdminOnboardingConfidence, canDeleteTestBusiness, getDeleteTestBusinessBlockedReason, isBusinessArchived } from '@/lib/admin-dashboard';
import { customerSetupStatusLabels, shouldShowCustomerSetupWaitingPage } from '@/lib/customer-setup';
import { buildAdminMissedCallValidationTruth, buildAdminOperationalProofs } from '@/lib/admin-operator-proof';
import { buildAdminNextStepGuide, buildAdminSetupPanels } from '@/lib/admin-setup-remediation';
import {
Expand Down Expand Up @@ -367,6 +368,7 @@ export default async function AdminBusinessDetailPage({
const nextStep = getStepByKey(setupFlow.steps, nextStepGuide.key);
const nextStepHref = buildStepPath(business.id, nextStepGuide.key, timelineFilter, activityExpanded);
const selectedStep = getStepByKey(setupFlow.steps, selectedStepKey);
const showPendingSetupBanner = shouldShowCustomerSetupWaitingPage(business.provisioningStatus);

const created = getQueryValue(searchParams, 'created') === '1';
const saved = getQueryValue(searchParams, 'saved') === '1';
Expand Down Expand Up @@ -1136,6 +1138,20 @@ export default async function AdminBusinessDetailPage({
{liveAcknowledged === 'warnings' ? 'Business marked live with explicit warning acknowledgment.' : 'Business marked live after launch checks.'}
</div>
) : null}
{showPendingSetupBanner ? (
<Card className="border-primary/20 bg-primary/5">
<CardHeader>
<CardTitle>This business is waiting for founder setup</CardTitle>
<CardDescription>
The owner should only see the setup-in-progress page until you finish the launch checklist and mark the workspace live.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-wrap items-center gap-3 text-sm">
<Badge variant="secondary">{customerSetupStatusLabels[business.provisioningStatus]}</Badge>
<span className="text-muted-foreground">Next action: {onboardingConfidence.nextAction}</span>
</CardContent>
</Card>
) : 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
2 changes: 2 additions & 0 deletions app/admin/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
import { logAuditEvent } from '@/lib/audit-log';
import { buildAdminTestSmsTruth, buildTwilioSetupUpdateEventMetadata } from '@/lib/admin-operator-visibility';
import { deriveTwilioNumberSetupModeFromPhoneSetupPath } from '@/lib/business-phone-setup';
import { sendCustomerReadyNotification } from '@/lib/customer-setup-handoff';
import { db } from '@/lib/db';
import { formatPhoneDetail, listBusinessOperatorEvents, maskSid, recordBusinessOperatorEvent } from '@/lib/operator-events';
import { maskPhoneForAudit, normalizePhoneNumber, normalizePhoneNumberToE164 } from '@/lib/phone';
Expand Down Expand Up @@ -1882,6 +1883,7 @@ export async function markBusinessLiveAction(formData: FormData) {
}

await updateBusinessProvisioningStatus(business.id, BusinessProvisioningStatus.LIVE, null);
await sendCustomerReadyNotification(business.id);
await recordBusinessOperatorEvent({
businessId: business.id,
type: confidence.canSafelyMarkLive ? 'admin.go_live_marked_safe' : 'admin.go_live_marked_with_warnings',
Expand Down
9 changes: 6 additions & 3 deletions app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Link from 'next/link';
import { BusinessProvisioningStatus } from '@prisma/client';

import {
archiveBusinessAction,
Expand Down Expand Up @@ -479,10 +480,10 @@ export default async function AdminPage({ searchParams }: { searchParams?: Recor
});

const summaryStats = [
{ label: 'Pending setup', value: businessRows.filter((row) => row.business.provisioningStatus === BusinessProvisioningStatus.DRAFT).length },
{ label: 'In setup', value: businessRows.filter((row) => row.business.provisioningStatus === BusinessProvisioningStatus.ONBOARDING).length },
{ label: 'Needs attention', value: businessRows.filter((row) => row.onboardingConfidence.state === 'needs_attention').length },
{ label: 'Waiting on compliance', value: businessRows.filter((row) => row.onboardingConfidence.state === 'waiting_on_a2p').length },
{ label: 'Ready for live', value: businessRows.filter((row) => row.onboardingConfidence.state === 'ready_to_go_live').length },
{ label: 'Live with warnings', value: businessRows.filter((row) => row.onboardingConfidence.state === 'live_with_warnings').length },
];
const founderResetBusinessCount = businessPickerOptions.length;
const founderResetBusinessPreview = businessPickerOptions.slice(0, 4).map((business) => business.name);
Expand Down Expand Up @@ -548,7 +549,7 @@ export default async function AdminPage({ searchParams }: { searchParams?: Recor
) : null}
{setupIntent === 'new-business-pilot' ? (
<div className="rounded-md border border-primary/30 bg-primary/5 p-3 text-sm">
Customer pilot setup starts here. Public signup is for business owners creating their own account; founder/operator setup for a real customer stays in this admin new-business flow.
New public pilot signups land here waiting for founder setup. Finish the owner details, launch prep, and go-live checks before the customer sees the full workspace.
</div>
) : null}

Expand Down Expand Up @@ -977,6 +978,8 @@ export default async function AdminPage({ searchParams }: { searchParams?: Recor
{business.name}
</Link>
<Badge variant={overallStatus.variant}>{overallStatus.label}</Badge>
{business.provisioningStatus === BusinessProvisioningStatus.DRAFT ? <Badge variant="secondary">Pending setup</Badge> : null}
{business.provisioningStatus === BusinessProvisioningStatus.ONBOARDING ? <Badge variant="outline">In setup</Badge> : null}
<Badge variant={onboardingConfidence.stateVariant}>{onboardingConfidence.stateLabel}</Badge>
{business.isTestBusiness ? <Badge variant="outline">Test</Badge> : null}
{isBusinessArchived(business) ? <Badge variant="outline">Archived</Badge> : null}
Expand Down
2 changes: 1 addition & 1 deletion app/api/stripe/checkout/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export async function POST(request: Request) {

const business = await db.business.findUnique({ where: { ownerClerkId: userId } });
if (!business) {
return withCorrelation(NextResponse.redirect(absoluteUrl('/app/onboarding'), { status: 303 }));
return withCorrelation(NextResponse.redirect(absoluteUrl('/app'), { status: 303 }));
}

const formData = await request.formData();
Expand Down
103 changes: 74 additions & 29 deletions app/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { auth } from '@clerk/nextjs/server';
import { auth, currentUser } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';
import Link from 'next/link';

import { AppNav } from '@/components/app-nav';
import { CustomerSetupWaitingPage } from '@/components/customer-setup-waiting-page';
import { Badge } from '@/components/ui/badge';
import { getAdminSession } from '@/lib/admin';
import { getAdminCustomerActingContext } from '@/lib/admin-customer-context';
import { buildAdminCustomerExitHref } from '@/lib/admin-customer-paths';
import { ensurePendingBusinessForOwner } from '@/lib/customer-setup-handoff';
import { getCustomerWorkspaceNotice, shouldShowCustomerSetupWaitingPage } from '@/lib/customer-setup';
import { db } from '@/lib/db';
import { getPortfolioDemoBusiness, isPortfolioDemoMode } from '@/lib/portfolio-demo';
import { getCustomerSystemStatus } from '@/lib/system-status';
Expand Down Expand Up @@ -49,43 +54,83 @@ export default async function AppLayout({ children }: { children: React.ReactNod
redirect('/sign-in');
}

const business = adminCustomerContext
const adminSession = await getAdminSession();

const existingBusiness = adminCustomerContext
? adminCustomerContext.business
: await db.business.findUnique({ where: { ownerClerkId: userId } });
if (!adminCustomerContext && adminSession?.isAdmin && !existingBusiness) {
redirect('/admin?intent=new-business-pilot');
}
const business =
existingBusiness ||
(adminCustomerContext
? null
: await (async () => {
const user = await currentUser();
const ownerEmail =
(user?.primaryEmailAddressId
? user.emailAddresses.find((email) => email.id === user.primaryEmailAddressId)?.emailAddress
: user?.emailAddresses[0]?.emailAddress) || null;

return ensurePendingBusinessForOwner(userId, {
businessName: typeof user?.publicMetadata?.businessName === 'string' ? user.publicMetadata.businessName : null,
ownerEmail,
ownerName: user?.fullName || [user?.firstName, user?.lastName].filter(Boolean).join(' ') || null,
});
})());
const successfulLeadCount = business
? await db.lead.count({ where: { businessId: business.id, OR: [{ ownerNotifiedAt: { not: null } }, { notifiedAt: { not: null } }] } })
: 0;
const systemStatus = business ? getCustomerSystemStatus(business, successfulLeadCount) : null;
const dashboardStatus = systemStatus ? getDashboardStatusPresentation(systemStatus) : null;
const workspaceNotice = business ? getCustomerWorkspaceNotice(business.provisioningStatus) : null;
const showWaitingPage = business ? shouldShowCustomerSetupWaitingPage(business.provisioningStatus) : false;

return (
<div className="min-h-screen">
<AppNav
business={business}
systemStatusLabel={dashboardStatus?.label ?? 'Setup in progress'}
systemStatusVariant={dashboardStatus?.badgeVariant ?? 'outline'}
/>
{adminCustomerContext && business ? (
<div className="border-b bg-primary/5">
<div className="container flex flex-col gap-3 py-3 text-sm lg:flex-row lg:items-center lg:justify-between">
<div>
<p className="font-medium">Admin customer mode</p>
<p className="text-muted-foreground">
You are using the real customer pages for <span className="font-medium text-foreground">{business.name}</span>.
</p>
</div>
<div className="flex flex-wrap gap-2">
<Link className="rounded-md border px-3 py-2 text-sm font-medium transition-colors hover:bg-background" href={`/admin/${business.id}`}>
Back to operator controls
</Link>
<Link className="rounded-md border px-3 py-2 text-sm font-medium transition-colors hover:bg-background" href={buildAdminCustomerExitHref(business.id)}>
Exit customer mode
</Link>
</div>
<div className="min-h-screen">
<AppNav
business={business}
systemStatusLabel={dashboardStatus?.label ?? 'Setup in progress'}
systemStatusVariant={dashboardStatus?.badgeVariant ?? 'outline'}
/>
{adminCustomerContext && business ? (
<div className="border-b bg-primary/5">
<div className="container flex flex-col gap-3 py-3 text-sm lg:flex-row lg:items-center lg:justify-between">
<div>
<p className="font-medium">Admin customer mode</p>
<p className="text-muted-foreground">
You are using the real customer pages for <span className="font-medium text-foreground">{business.name}</span>.
</p>
</div>
<div className="flex flex-wrap gap-2">
<Link className="rounded-md border px-3 py-2 text-sm font-medium transition-colors hover:bg-background" href={`/admin/${business.id}`}>
Back to operator controls
</Link>
<Link className="rounded-md border px-3 py-2 text-sm font-medium transition-colors hover:bg-background" href={buildAdminCustomerExitHref(business.id)}>
Exit customer mode
</Link>
</div>
</div>
) : null}
<main className="container py-8">{children}</main>
</div>
);
</div>
) : null}
<main className="container py-8">
{showWaitingPage && business ? (
<CustomerSetupWaitingPage businessName={business.name} status={business.provisioningStatus} />
) : (
<>
{workspaceNotice ? (
<div className="mb-6 rounded-2xl border bg-background/80 p-4">
<div className="flex flex-wrap items-center gap-3">
<Badge variant={workspaceNotice.variant}>{workspaceNotice.title}</Badge>
<p className="text-sm text-muted-foreground">{workspaceNotice.detail}</p>
</div>
</div>
) : null}
{children}
</>
)}
</main>
</div>
);
}
Loading
Loading