diff --git a/ee/apps/den-api/src/organization-limits.ts b/ee/apps/den-api/src/organization-limits.ts index 017535576..64e9f3656 100644 --- a/ee/apps/den-api/src/organization-limits.ts +++ b/ee/apps/den-api/src/organization-limits.ts @@ -1,5 +1,5 @@ -import { eq, sql } from "@openwork-ee/den-db/drizzle" -import { MemberTable, OrganizationTable, WorkerTable } from "@openwork-ee/den-db/schema" +import { and, eq, gt, sql } from "@openwork-ee/den-db/drizzle" +import { InvitationTable, MemberTable, OrganizationTable, WorkerTable } from "@openwork-ee/den-db/schema" import { db } from "./db.js" export const DEFAULT_ORGANIZATION_LIMITS = { @@ -116,6 +116,15 @@ async function countOrganizationMembers(organizationId: OrganizationId) { return Number(rows[0]?.count ?? 0) } +async function countPendingOrganizationInvitations(organizationId: OrganizationId) { + const rows = await db + .select({ count: sql`count(*)` }) + .from(InvitationTable) + .where(and(eq(InvitationTable.organizationId, organizationId), eq(InvitationTable.status, "pending"), gt(InvitationTable.expiresAt, new Date()))) + + return Number(rows[0]?.count ?? 0) +} + async function countOrganizationWorkers(organizationId: OrganizationId) { const rows = await db .select({ count: sql`count(*)` }) @@ -129,7 +138,7 @@ export async function getOrganizationLimitStatus(organizationId: OrganizationId, const metadata = await getOrInitializeOrganizationMetadata(organizationId) const currentCount = limitType === "members" - ? await countOrganizationMembers(organizationId) + ? (await countOrganizationMembers(organizationId)) + (await countPendingOrganizationInvitations(organizationId)) : await countOrganizationWorkers(organizationId) const limit = metadata.limits[limitType] diff --git a/ee/apps/den-api/src/routes/org/invitations.ts b/ee/apps/den-api/src/routes/org/invitations.ts index 7e453b557..e74dd0a32 100644 --- a/ee/apps/den-api/src/routes/org/invitations.ts +++ b/ee/apps/den-api/src/routes/org/invitations.ts @@ -51,17 +51,6 @@ export function registerOrgInvitationRoutes([]); const [busy, setBusy] = useState(true); const [error, setError] = useState(null); @@ -20,6 +66,7 @@ export function OrganizationScreen() { const [createName, setCreateName] = useState(""); const [createBusy, setCreateBusy] = useState(false); const [createError, setCreateError] = useState(null); + const [hasPendingOrgDraft, setHasPendingOrgDraft] = useState(false); const userDisplayName = useMemo(() => { const trimmedName = user?.name?.trim(); @@ -35,6 +82,8 @@ export function OrganizationScreen() { const activeOrg = useMemo(() => orgs.find((org) => org.isActive) ?? null, [orgs]); const showDirectCreateFlow = orgs.length === 0; + const returningToSavedDraft = showDirectCreateFlow && hasPendingOrgDraft; + const draftReadyAfterCheckout = returningToSavedDraft && Boolean(billingSummary?.hasActivePlan); useEffect(() => { if (!sessionHydrated) return; @@ -74,6 +123,28 @@ export function OrganizationScreen() { }; }, [sessionHydrated, user, router]); + useEffect(() => { + if (!user?.email) { + setHasPendingOrgDraft(false); + return; + } + + if (!showDirectCreateFlow) { + clearPendingOrgDraft(); + setHasPendingOrgDraft(false); + return; + } + + const savedName = readPendingOrgDraft(user.email); + if (!savedName) { + setHasPendingOrgDraft(false); + return; + } + + setCreateName((current) => current || savedName); + setHasPendingOrgDraft(true); + }, [showDirectCreateFlow, user?.email]); + async function handleCreate(e: React.FormEvent) { e.preventDefault(); const trimmed = createName.trim(); @@ -89,6 +160,8 @@ export function OrganizationScreen() { if (!response.ok) { if (response.status === 402) { + writePendingOrgDraft(trimmed, user?.email); + setHasPendingOrgDraft(true); setCreateBusy(false); router.push("/checkout"); return; @@ -106,6 +179,8 @@ export function OrganizationScreen() { throw new Error("Organization was created, but no slug was returned."); } + clearPendingOrgDraft(); + setHasPendingOrgDraft(false); router.push(getOrgDashboardRoute(nextSlug)); } catch (err) { setCreateError(err instanceof Error ? err.message : "Failed to create organization."); @@ -153,9 +228,15 @@ export function OrganizationScreen() {

OpenWork Cloud

-

Create your first organization.

+

+ {returningToSavedDraft ? "Finish creating your organization." : "Create your first organization."} +

- Name the workspace you want to set up for your team. If billing is enabled, the plan step comes right after this. + {draftReadyAfterCheckout + ? "Your plan is ready. Confirm the workspace name below and create the workspace to finish setup." + : returningToSavedDraft + ? "We saved your workspace name so you can pick up right where you left off." + : "Name the workspace you want to set up for your team. If billing is enabled, the plan step comes right after this."}

@@ -188,7 +269,7 @@ export function OrganizationScreen() { disabled={createBusy || !createName.trim()} className="rounded-2xl bg-gray-900 px-5 py-3 text-sm font-medium text-white transition-colors hover:bg-gray-800 disabled:opacity-50" > - {createBusy ? "Creating..." : "Continue"} + {createBusy ? "Creating..." : returningToSavedDraft ? "Create organization" : "Continue"}

Signed in as {user?.email ?? "your account"}