[email protected] is a fixed, non-deliverable placeholder (no mail relay serves that domain, and Mailpit was removed a while back) - prefilling it was just noise the local dev would always need to clear. Real accounts still get their own email prefilled as before. Co-Authored-By: Claude Sonnet 5 <[email protected]>
698 lines
27 KiB
TypeScript
698 lines
27 KiB
TypeScript
"use client";
|
|
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import Link from "next/link";
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
import { Suspense, useEffect, useState } from "react";
|
|
import { Controller, useForm } from "react-hook-form";
|
|
import { Loader2 } from "lucide-react";
|
|
import { z } from "zod";
|
|
import { FormField } from "@/components/ui/form-field";
|
|
import { Select } from "@/components/ui/select";
|
|
import { api } from "@/lib/api-client";
|
|
import { useCompanies, useCreateCompany } from "@/hooks/use-companies";
|
|
import { useCreateNotificationDestination } from "@/hooks/use-notification-destinations";
|
|
import { useDiscoverCompany } from "@/hooks/use-discovery";
|
|
import { authErrorMessage, useCurrentUser, useSystemStatus } from "@/hooks/use-auth";
|
|
import { isLocalConvenience } from "@/lib/auth";
|
|
import {
|
|
FREQUENCY_LABELS,
|
|
MONITORING_FREQUENCIES,
|
|
SEVERITY_LABELS,
|
|
SEVERITY_LEVELS,
|
|
type CompanyResponse,
|
|
type DiscoveredCompanyProfile,
|
|
type MonitoringFrequency,
|
|
type SeverityLevel,
|
|
} from "@/lib/types";
|
|
|
|
const _LEGAL_SUFFIX_RE = /\b(inc|incorporated|llc|ltd|limited|corp|corporation|co|company|plc)\b/g;
|
|
|
|
function normalizeCompanyName(name: string): string {
|
|
return name
|
|
.toLowerCase()
|
|
.replace(/[.,]/g, "")
|
|
.replace(_LEGAL_SUFFIX_RE, "")
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|
|
}
|
|
|
|
function findPossibleDuplicate(
|
|
name: string,
|
|
companies: CompanyResponse[],
|
|
): CompanyResponse | undefined {
|
|
const normalized = normalizeCompanyName(name);
|
|
if (!normalized) return undefined;
|
|
return companies.find((c) => {
|
|
const existing = normalizeCompanyName(c.name);
|
|
return (
|
|
existing.length > 0 &&
|
|
(existing === normalized || existing.includes(normalized) || normalized.includes(existing))
|
|
);
|
|
});
|
|
}
|
|
|
|
const companySchema = z
|
|
.object({
|
|
name: z.string().min(1, "Company name is required").max(200),
|
|
officialWebsite: z.string().max(500).optional().or(z.literal("")),
|
|
industry: z.string().max(120).optional().or(z.literal("")),
|
|
country: z.string().max(120).optional().or(z.literal("")),
|
|
region: z.string().max(120).optional().or(z.literal("")),
|
|
headquarters: z.string().max(200).optional().or(z.literal("")),
|
|
description: z.string().max(4000).optional().or(z.literal("")),
|
|
monitoringFocus: z.string().max(2000).optional().or(z.literal("")),
|
|
competitorNames: z.string().optional().or(z.literal("")),
|
|
aliasNames: z.string().optional().or(z.literal("")),
|
|
frequencyType: z.enum(MONITORING_FREQUENCIES),
|
|
intervalMinutes: z.coerce.number().int().positive().optional(),
|
|
cronExpression: z.string().max(120).optional().or(z.literal("")),
|
|
timezone: z.string().min(1).max(64),
|
|
severityThreshold: z.enum(SEVERITY_LEVELS),
|
|
notificationEmail: z.string().email("Enter a valid email address"),
|
|
consent: z.literal(true, {
|
|
errorMap: () => ({ message: "You must acknowledge the collection policy to continue" }),
|
|
}),
|
|
})
|
|
.refine(
|
|
(data) =>
|
|
data.frequencyType !== "custom" ||
|
|
Boolean(data.intervalMinutes) ||
|
|
Boolean(data.cronExpression),
|
|
{
|
|
message: "Provide either an interval in minutes or a cron expression for a custom schedule",
|
|
path: ["intervalMinutes"],
|
|
},
|
|
);
|
|
|
|
type CompanyForm = z.infer<typeof companySchema>;
|
|
|
|
const STEPS = ["Discover", "Review", "Schedule", "Notifications", "Confirm"] as const;
|
|
|
|
const STEP_FIELDS: Record<number, (keyof CompanyForm)[]> = {
|
|
0: ["name"],
|
|
1: [],
|
|
2: ["frequencyType", "intervalMinutes", "cronExpression", "timezone"],
|
|
3: ["severityThreshold", "notificationEmail", "consent"],
|
|
4: [],
|
|
};
|
|
|
|
function splitNames(value: string | undefined): string[] {
|
|
return (value ?? "")
|
|
.split(",")
|
|
.map((v) => v.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
const MONITORING_FOCUS_PLACEHOLDER =
|
|
"E.g. new product or pricing changes, leadership hires/departures, hiring trends, " +
|
|
"funding/financial signals, partnerships and acquisitions, patents, manufacturing or " +
|
|
"expansion moves, regulatory or legal news, and how they compare to competitors.";
|
|
|
|
function AddCompanyWizard() {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const prefillName = searchParams.get("name") ?? "";
|
|
const returnTo = searchParams.get("returnTo");
|
|
const [step, setStep] = useState(0);
|
|
const [discovery, setDiscovery] = useState<DiscoveredCompanyProfile | null>(null);
|
|
const [isFinalizing, setIsFinalizing] = useState(false);
|
|
const [duplicateWarning, setDuplicateWarning] = useState<{
|
|
company: CompanyResponse;
|
|
forName: string;
|
|
} | null>(null);
|
|
const [acknowledgedDuplicateFor, setAcknowledgedDuplicateFor] = useState<string | null>(null);
|
|
const { data: allCompanies } = useCompanies();
|
|
const createCompany = useCreateCompany();
|
|
const createDestination = useCreateNotificationDestination();
|
|
const discoverCompany = useDiscoverCompany();
|
|
const { data: systemStatus } = useSystemStatus();
|
|
const { data: currentUser } = useCurrentUser();
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
control,
|
|
watch,
|
|
setValue,
|
|
getValues,
|
|
trigger,
|
|
formState: { errors, dirtyFields },
|
|
} = useForm<CompanyForm>({
|
|
resolver: zodResolver(companySchema),
|
|
defaultValues: {
|
|
name: prefillName,
|
|
frequencyType: "weekly",
|
|
timezone: "America/New_York",
|
|
severityThreshold: "medium",
|
|
},
|
|
});
|
|
|
|
// Prefill with the account's own email once it loads - `defaultValues`
|
|
// can't do this since useCurrentUser resolves after the form has already
|
|
// mounted. Only sets it while the field is still untouched, so it never
|
|
// clobbers something the user already typed. Skipped for the local-dev
|
|
// bypass account: its email is a fixed, non-deliverable placeholder
|
|
// ([email protected] - no mail relay actually serves that domain),
|
|
// not something worth prefilling - local dev just types whatever real
|
|
// address they want to test with.
|
|
const isLocalDev = systemStatus ? isLocalConvenience(systemStatus) : false;
|
|
useEffect(() => {
|
|
if (currentUser?.email && !isLocalDev && !dirtyFields.notificationEmail) {
|
|
setValue("notificationEmail", currentUser.email);
|
|
}
|
|
}, [currentUser?.email, isLocalDev, dirtyFields.notificationEmail, setValue]);
|
|
|
|
const frequencyType = watch("frequencyType");
|
|
|
|
const goNext = async () => {
|
|
const valid = await trigger(STEP_FIELDS[step]);
|
|
if (valid) setStep((s) => Math.min(s + 1, STEPS.length - 1));
|
|
};
|
|
const goBack = () => {
|
|
if (step === 0) {
|
|
// Nothing earlier in the wizard to step back to - leave to wherever
|
|
// the user actually came from (Dashboard, Companies, or a rival
|
|
// company's page via the "add this competitor" link). A `returnTo`
|
|
// param (set by the company page's competitor/customer pills) is
|
|
// preferred over plain browser-history back, since a client
|
|
// component's local state (e.g. which tab was open) doesn't reliably
|
|
// survive a back-navigation in the App Router.
|
|
if (returnTo) {
|
|
router.push(returnTo);
|
|
return;
|
|
}
|
|
router.back();
|
|
return;
|
|
}
|
|
setStep((s) => Math.max(s - 1, 0));
|
|
};
|
|
|
|
const handleDiscover = async () => {
|
|
const valid = await trigger(["name"]);
|
|
if (!valid) return;
|
|
|
|
const values = getValues();
|
|
|
|
if (acknowledgedDuplicateFor !== values.name) {
|
|
const possibleDuplicate = findPossibleDuplicate(values.name, allCompanies ?? []);
|
|
if (possibleDuplicate) {
|
|
setDuplicateWarning({ company: possibleDuplicate, forName: values.name });
|
|
return;
|
|
}
|
|
}
|
|
setDuplicateWarning(null);
|
|
|
|
const profile = await discoverCompany.mutateAsync({
|
|
name: values.name,
|
|
official_website: values.officialWebsite || undefined,
|
|
monitoring_focus: values.monitoringFocus || undefined,
|
|
competitor_names: splitNames(values.competitorNames),
|
|
alias_names: splitNames(values.aliasNames),
|
|
});
|
|
|
|
setDiscovery(profile);
|
|
setValue("officialWebsite", profile.official_website ?? "");
|
|
setValue("description", profile.description ?? "");
|
|
setValue("industry", profile.industry ?? "");
|
|
setValue("country", profile.country ?? "");
|
|
setValue("region", profile.region ?? "");
|
|
setValue("headquarters", profile.headquarters ?? "");
|
|
setValue("monitoringFocus", profile.monitoring_focus ?? values.monitoringFocus ?? "");
|
|
setValue("competitorNames", profile.competitors.join(", "));
|
|
setValue("aliasNames", profile.aliases.join(", "));
|
|
setStep(1);
|
|
};
|
|
|
|
const onSubmit = handleSubmit(async (values) => {
|
|
setIsFinalizing(true);
|
|
try {
|
|
const company = await createCompany.mutateAsync({
|
|
name: values.name,
|
|
official_website: values.officialWebsite || null,
|
|
industry: values.industry || null,
|
|
country: values.country || null,
|
|
region: values.region || null,
|
|
headquarters: values.headquarters || null,
|
|
public_identifiers: discovery?.public_identifiers ?? {},
|
|
description: values.description || null,
|
|
monitoring_focus: values.monitoringFocus || null,
|
|
competitor_names: splitNames(values.competitorNames),
|
|
alias_names: splitNames(values.aliasNames),
|
|
frequency_type: values.frequencyType,
|
|
interval_minutes:
|
|
values.frequencyType === "custom" ? (values.intervalMinutes ?? null) : null,
|
|
cron_expression: values.frequencyType === "custom" ? values.cronExpression || null : null,
|
|
timezone: values.timezone,
|
|
severity_threshold: values.severityThreshold,
|
|
});
|
|
|
|
await createDestination
|
|
.mutateAsync({
|
|
type: "email",
|
|
destination_value: values.notificationEmail,
|
|
company_ids: [company.id],
|
|
})
|
|
.catch(() => undefined); // Best-effort; company creation still succeeded either way.
|
|
|
|
// Best-effort, same as the destination above - a failed enqueue here
|
|
// shouldn't block landing on the new company's page, it just means
|
|
// the user sees an idle Run Now button instead of the running state.
|
|
await api.runCompanyNow(company.id).catch(() => undefined);
|
|
|
|
router.push(`/companies/${company.id}`);
|
|
} catch {
|
|
setIsFinalizing(false);
|
|
}
|
|
});
|
|
|
|
return (
|
|
<div className="mx-auto max-w-2xl">
|
|
<h1 className="text-2xl font-semibold text-slate-900">Add a company</h1>
|
|
<p className="mt-1 text-sm text-slate-600">
|
|
Give us a name — we'll find the rest and let you correct anything before we start
|
|
monitoring.
|
|
</p>
|
|
|
|
<ol className="mt-6 flex items-center gap-2 text-xs font-medium text-slate-500">
|
|
{STEPS.map((label, i) => (
|
|
<li
|
|
key={label}
|
|
className={`flex items-center gap-2 transition-colors duration-300 ${i === step ? "text-brand-600" : ""}`}
|
|
>
|
|
<span
|
|
className={`flex h-5 w-5 items-center justify-center rounded-full border text-[11px] transition-all duration-300 ${
|
|
i === step
|
|
? "scale-110 border-brand-600 bg-brand-600 text-white"
|
|
: i < step
|
|
? "border-brand-600 text-brand-600"
|
|
: "border-slate-300"
|
|
}`}
|
|
>
|
|
{i + 1}
|
|
</span>
|
|
{label}
|
|
{i < STEPS.length - 1 && <span className="mx-1 text-slate-300">/</span>}
|
|
</li>
|
|
))}
|
|
</ol>
|
|
|
|
<form
|
|
onSubmit={onSubmit}
|
|
className="mt-6 space-y-5 rounded-lg border border-slate-200 bg-white p-6"
|
|
noValidate
|
|
>
|
|
<div key={step} className="animate-fade-in space-y-5">
|
|
{step === 0 && (
|
|
<div className="space-y-4">
|
|
<FormField label="Company name" {...register("name")} error={errors.name?.message} />
|
|
|
|
{duplicateWarning && duplicateWarning.forName === watch("name") && (
|
|
<div className="animate-fade-in rounded-md border border-amber-300 bg-amber-50 p-4 text-sm text-amber-900">
|
|
<p>
|
|
You might already be monitoring{" "}
|
|
<strong>"{duplicateWarning.company.name}"</strong>. Adding another
|
|
with a similar name is fine, but double-check it's not the same company.
|
|
</p>
|
|
<div className="mt-3 flex flex-wrap items-center gap-3">
|
|
<Link
|
|
href={`/companies/${duplicateWarning.company.id}`}
|
|
className="focus-ring rounded-md border border-amber-400 bg-white px-3 py-1.5 text-xs font-semibold text-amber-900 hover:bg-amber-100"
|
|
>
|
|
View existing company
|
|
</Link>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setAcknowledgedDuplicateFor(duplicateWarning.forName);
|
|
setDuplicateWarning(null);
|
|
}}
|
|
className="focus-ring rounded-md px-3 py-1.5 text-xs font-semibold text-amber-900 underline decoration-amber-400 underline-offset-2 hover:bg-amber-100"
|
|
>
|
|
Continue anyway
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="rounded-md border border-dashed border-slate-300 p-4">
|
|
<p className="text-sm font-medium text-slate-700">
|
|
Optional — helps discovery be more accurate
|
|
</p>
|
|
<div className="mt-3 space-y-4">
|
|
<FormField
|
|
label="Official website"
|
|
placeholder="example.com"
|
|
hint="If you know it, we'll skip searching for it."
|
|
{...register("officialWebsite")}
|
|
error={errors.officialWebsite?.message}
|
|
/>
|
|
<div>
|
|
<label
|
|
htmlFor="monitoringFocus"
|
|
className="block text-sm font-medium text-slate-700"
|
|
>
|
|
What do you want to know?
|
|
</label>
|
|
<textarea
|
|
id="monitoringFocus"
|
|
rows={3}
|
|
placeholder={MONITORING_FOCUS_PLACEHOLDER}
|
|
className="focus-ring mt-1 block w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm"
|
|
{...register("monitoringFocus")}
|
|
/>
|
|
</div>
|
|
<FormField
|
|
label="Known competitors (comma-separated)"
|
|
placeholder="Rival Motors, Other Corp"
|
|
{...register("competitorNames")}
|
|
/>
|
|
<FormField
|
|
label="Known aliases (comma-separated)"
|
|
placeholder="Brand name, subsidiary name"
|
|
{...register("aliasNames")}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{discoverCompany.isError && (
|
|
<p className="text-sm text-red-600" role="alert">
|
|
{authErrorMessage(discoverCompany.error)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{step === 1 && (
|
|
<div className="space-y-4">
|
|
<p className="text-sm text-slate-600">
|
|
Here's what we found for <strong>{watch("name")}</strong>. Edit anything before
|
|
continuing — fields we couldn't find are left blank for you to fill in.
|
|
</p>
|
|
<FormField
|
|
label="Official website"
|
|
placeholder="example.com"
|
|
{...register("officialWebsite")}
|
|
error={errors.officialWebsite?.message}
|
|
/>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<FormField
|
|
label="Industry"
|
|
placeholder="Not found — fill in"
|
|
{...register("industry")}
|
|
/>
|
|
<FormField
|
|
label="Headquarters"
|
|
placeholder="Not found — fill in"
|
|
{...register("headquarters")}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<FormField
|
|
label="Country"
|
|
placeholder="Not found — fill in"
|
|
{...register("country")}
|
|
/>
|
|
<FormField
|
|
label="Region"
|
|
placeholder="Not found — fill in"
|
|
{...register("region")}
|
|
/>
|
|
</div>
|
|
<FormField
|
|
label="Competitors (comma-separated)"
|
|
placeholder="Rival Motors, Other Corp"
|
|
{...register("competitorNames")}
|
|
/>
|
|
<FormField
|
|
label="Known aliases (comma-separated)"
|
|
placeholder="Brand name, subsidiary name"
|
|
{...register("aliasNames")}
|
|
/>
|
|
<div>
|
|
<label htmlFor="description" className="block text-sm font-medium text-slate-700">
|
|
Description
|
|
</label>
|
|
<textarea
|
|
id="description"
|
|
rows={2}
|
|
placeholder="Not found — fill in"
|
|
className="focus-ring mt-1 block w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm"
|
|
{...register("description")}
|
|
/>
|
|
</div>
|
|
|
|
{discovery && discovery.potential_sources.length > 0 && (
|
|
<div className="rounded-md bg-slate-50 p-4">
|
|
<p className="text-sm font-medium text-slate-700">
|
|
We'll start monitoring {discovery.potential_sources.length} source
|
|
{discovery.potential_sources.length === 1 ? "" : "s"}
|
|
</p>
|
|
<ul className="mt-2 space-y-1 text-xs text-slate-600">
|
|
{discovery.potential_sources.map((source, i) => (
|
|
<li key={i}>
|
|
{source.name} — {source.base_url ?? "n/a"}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
{discovery && Object.keys(discovery.public_identifiers).length > 0 && (
|
|
<p className="text-xs text-slate-500">
|
|
{Object.entries(discovery.public_identifiers).map(([key, value]) => (
|
|
<span key={key} className="mr-3">
|
|
{key}: {value}
|
|
</span>
|
|
))}
|
|
</p>
|
|
)}
|
|
|
|
{discovery && discovery.sources_consulted.length > 0 && (
|
|
<p className="text-xs text-slate-400">
|
|
Sources consulted: {discovery.sources_consulted.join(", ")}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{step === 2 && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700">
|
|
Monitoring frequency
|
|
</label>
|
|
<div className="mt-1">
|
|
<Select
|
|
value={frequencyType}
|
|
onValueChange={(v) => setValue("frequencyType", v as MonitoringFrequency)}
|
|
options={MONITORING_FREQUENCIES.map((f: MonitoringFrequency) => ({
|
|
value: f,
|
|
label: FREQUENCY_LABELS[f],
|
|
}))}
|
|
triggerClassName="w-full px-3 py-2 text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
{frequencyType === "custom" && (
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<FormField
|
|
label="Interval (minutes)"
|
|
type="number"
|
|
{...register("intervalMinutes")}
|
|
error={errors.intervalMinutes?.message}
|
|
/>
|
|
<FormField
|
|
label="Cron expression (advanced)"
|
|
placeholder="0 9 * * MON"
|
|
{...register("cronExpression")}
|
|
/>
|
|
</div>
|
|
)}
|
|
<FormField
|
|
label="Timezone"
|
|
hint="IANA timezone name, e.g. America/New_York"
|
|
{...register("timezone")}
|
|
error={errors.timezone?.message}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{step === 3 && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700">
|
|
Alert severity threshold
|
|
</label>
|
|
<div className="mt-1">
|
|
<Select
|
|
value={watch("severityThreshold")}
|
|
onValueChange={(v) => setValue("severityThreshold", v as SeverityLevel)}
|
|
options={SEVERITY_LEVELS.map((s: SeverityLevel) => ({
|
|
value: s,
|
|
label: SEVERITY_LABELS[s],
|
|
}))}
|
|
triggerClassName="w-full px-3 py-2 text-sm"
|
|
/>
|
|
</div>
|
|
<p className="mt-1 text-xs text-slate-500">
|
|
You'll be notified for this severity and above.
|
|
</p>
|
|
</div>
|
|
<FormField
|
|
label="Notification email"
|
|
type="email"
|
|
{...register("notificationEmail")}
|
|
error={errors.notificationEmail?.message}
|
|
/>
|
|
<Controller
|
|
name="consent"
|
|
control={control}
|
|
render={({ field }) => (
|
|
<label className="flex items-start gap-2 text-sm text-slate-700">
|
|
<input
|
|
type="checkbox"
|
|
className="mt-0.5"
|
|
checked={field.value === true}
|
|
onChange={(e) => field.onChange(e.target.checked)}
|
|
/>
|
|
<span>
|
|
I understand CI Agent only collects publicly available information and will
|
|
never bypass logins, CAPTCHAs, paywalls, or anti-bot controls.
|
|
</span>
|
|
</label>
|
|
)}
|
|
/>
|
|
{errors.consent && (
|
|
<p className="text-sm text-red-600" role="alert">
|
|
{errors.consent.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{step === 4 && isFinalizing && (
|
|
<div className="flex flex-col items-center justify-center gap-3 py-10 text-sm text-slate-600">
|
|
<Loader2 className="h-6 w-6 animate-spin text-brand-600" aria-hidden />
|
|
<p>Setting up monitoring for {watch("name")}…</p>
|
|
</div>
|
|
)}
|
|
|
|
{step === 4 && !isFinalizing && (
|
|
<div className="space-y-2 text-sm">
|
|
<p>
|
|
<span className="font-medium text-slate-700">Company:</span> {watch("name")}
|
|
</p>
|
|
<p>
|
|
<span className="font-medium text-slate-700">Website:</span>{" "}
|
|
{watch("officialWebsite") || "—"}
|
|
</p>
|
|
<p>
|
|
<span className="font-medium text-slate-700">Industry:</span>{" "}
|
|
{watch("industry") || "—"}
|
|
</p>
|
|
<p>
|
|
<span className="font-medium text-slate-700">Headquarters:</span>{" "}
|
|
{watch("headquarters") || "—"}
|
|
</p>
|
|
<p>
|
|
<span className="font-medium text-slate-700">Region:</span>{" "}
|
|
{[watch("country"), watch("region")].filter(Boolean).join(", ") || "—"}
|
|
</p>
|
|
<p>
|
|
<span className="font-medium text-slate-700">Competitors:</span>{" "}
|
|
{watch("competitorNames") || "—"}
|
|
</p>
|
|
<p>
|
|
<span className="font-medium text-slate-700">Aliases:</span>{" "}
|
|
{watch("aliasNames") || "—"}
|
|
</p>
|
|
<p>
|
|
<span className="font-medium text-slate-700">Focus:</span>{" "}
|
|
{watch("monitoringFocus") || "—"}
|
|
</p>
|
|
<p>
|
|
<span className="font-medium text-slate-700">Frequency:</span>{" "}
|
|
{FREQUENCY_LABELS[watch("frequencyType")]}
|
|
</p>
|
|
<p>
|
|
<span className="font-medium text-slate-700">Severity threshold:</span>{" "}
|
|
{SEVERITY_LABELS[watch("severityThreshold")]}
|
|
</p>
|
|
<p>
|
|
<span className="font-medium text-slate-700">Email:</span>{" "}
|
|
{watch("notificationEmail")}
|
|
</p>
|
|
|
|
{systemStatus?.ninjapear_configured && (
|
|
<p className="rounded-md bg-slate-50 p-3 text-xs text-slate-600">
|
|
NinjaPear company enrichment is on:{" "}
|
|
{systemStatus.ninjapear_credit_balance !== null
|
|
? `${systemStatus.ninjapear_credit_balance} credits remaining, `
|
|
: ""}
|
|
this company will use up to ~
|
|
{systemStatus.ninjapear_estimated_credits_per_company ?? "?"} credits.
|
|
</p>
|
|
)}
|
|
|
|
{createCompany.isError && (
|
|
<p className="text-sm text-red-600" role="alert">
|
|
{authErrorMessage(createCompany.error)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between border-t border-slate-100 pt-4">
|
|
<button
|
|
type="button"
|
|
onClick={goBack}
|
|
disabled={isFinalizing}
|
|
className="focus-ring rounded-md px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100 disabled:opacity-40"
|
|
>
|
|
Back
|
|
</button>
|
|
{step === 0 ? (
|
|
<button
|
|
type="button"
|
|
onClick={handleDiscover}
|
|
disabled={discoverCompany.isPending}
|
|
className="focus-ring inline-flex items-center gap-2 rounded-md bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700 disabled:opacity-60"
|
|
>
|
|
{discoverCompany.isPending && (
|
|
<Loader2 className="h-4 w-4 animate-spin" aria-hidden />
|
|
)}
|
|
{discoverCompany.isPending ? "Discovering…" : "Discover company"}
|
|
</button>
|
|
) : step < STEPS.length - 1 ? (
|
|
<button
|
|
type="button"
|
|
onClick={goNext}
|
|
className="focus-ring rounded-md bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700"
|
|
>
|
|
Continue
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="submit"
|
|
disabled={isFinalizing}
|
|
className="focus-ring rounded-md bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700 disabled:opacity-60"
|
|
>
|
|
{isFinalizing ? "Creating…" : "Create company"}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function AddCompanyWizardPage() {
|
|
return (
|
|
<Suspense fallback={null}>
|
|
<AddCompanyWizard />
|
|
</Suspense>
|
|
);
|
|
}
|