feat(web): telas de negócio reais no EDEN Core (Empresas, Produtos, Revendas, CRM, Ofertas)

Prioriza a parte web a pedido do usuário. 2 subagentes eden-frontend em
paralelo sobre o shell autenticado já pronto:

- Empresas/Produtos/Revendas: CRUD completo (lista + criar/editar),
  layout modelado nas páginas reais do tema DreamsERP comprado
  (purchase/vendors.tsx, inventory/products.tsx) — máscara de moeda para
  as 5 faixas de preço + implantação. Novo componente Table<T> genérico
  em packages/ui.
- CRM: Leads (lista, criar/editar, converter em oportunidade) e
  Oportunidades, modelados em crm/leads.tsx/dealsDetails.tsx.
- Ofertas/Quotes: o módulo mais rico — criação com lista dinâmica de
  itens produto/quantidade, fidelidade reduzida opcional, desconto
  opcional, totais calculados pelo servidor; detalhe com aprovar/rejeitar
  e fechar negócio. Modelado em sales/salesQuotes*.tsx. Novo componente
  Select em packages/ui.

Correção de instrução no meio da leva: usuário pediu para seguir de
perto o layout real do tema comprado, não só os tokens visuais já
extraídos — as páginas React relevantes do tema foram extraídas para
.tmp/theme-reference/ (nunca versionado) em cada worktree e os agentes
já em execução foram redirecionados via mensagem (não relançados).

Bug real do próprio agente, corrigindo meu brief: Leads/Opportunities
respondem em camelCase (mapLead/mapOpportunity), não snake_case como o
resto da API — verificado contra o código-fonte real antes de aceitar.

Integração manual: um dos 2 agentes não conseguiu git merge no próprio
sandbox e restaurou o shell existente por leitura+escrita — cada arquivo
restaurado foi comparado byte a byte contra main (diff -rq) antes de
aceitar. App.tsx/AuthenticatedLayout.tsx/packages/ui/index.ts
reconciliados manualmente juntando as rotas/exports dos dois agentes.

Verificado: pnpm turbo run typecheck build (28/28); containers
eden-api/eden-core reconstruídos; bundle de produção contém todas as
novas telas; login simulando Origin de browser real; listagens
retornando dados reais; criação de lead confirmando o formato camelCase.

Claude-Session: https://claude.ai/code/session_01BcWWuKfykGtUweXmHD6cKx

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 18:31:07 -03:00
parent 4d67fe5f1a
commit f96c465ee5
31 changed files with 3493 additions and 5 deletions

View File

@@ -0,0 +1,138 @@
import { useEffect, useState } from "react";
import type { FormEvent } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Alert, Button, Card, Input, PageHeader, Select } from "@eden/ui";
import { useAuth } from "../../lib/auth/AuthContext.js";
import { ApiError } from "../../lib/api/http.js";
import { convertLead, getLead } from "../../lib/api/leads.js";
import type { Lead } from "../../lib/api/leads.js";
import { OPPORTUNITY_STAGES } from "../../lib/api/opportunities.js";
import type { OpportunityStage } from "../../lib/api/opportunities.js";
import { Breadcrumb } from "../../components/Breadcrumb.js";
const STAGE_LABEL: Record<OpportunityStage, string> = {
prospecting: "Prospecção",
qualification: "Qualificação",
proposal: "Proposta",
negotiation: "Negociação",
won: "Ganha",
lost: "Perdida",
};
const STAGE_OPTIONS = OPPORTUNITY_STAGES.map((stage) => ({ value: stage, label: STAGE_LABEL[stage] }));
export function ConvertLeadPage() {
const { id } = useParams<{ id: string }>();
const { accessToken } = useAuth();
const navigate = useNavigate();
const [lead, setLead] = useState<Lead | null>(null);
const [title, setTitle] = useState("");
const [stage, setStage] = useState<OpportunityStage>("prospecting");
const [probability, setProbability] = useState("");
const [notes, setNotes] = useState("");
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!id || !accessToken) return;
setLoading(true);
getLead(id, accessToken)
.then((result) => {
setLead(result);
setTitle(`${result.contactName}${result.companyName ? ` — ${result.companyName}` : ""}`);
})
.catch((err) => setError(err instanceof ApiError ? err.message : "Não foi possível carregar o lead."))
.finally(() => setLoading(false));
}, [id, accessToken]);
async function handleSubmit(event: FormEvent) {
event.preventDefault();
if (!accessToken || !id) return;
setError(null);
setSubmitting(true);
try {
const result = await convertLead(
id,
{
title,
stage,
probability: probability ? Number(probability) : undefined,
notes: notes || undefined,
},
accessToken,
);
navigate(`/opportunities/${result.opportunity.id}/edit`, { replace: true });
} catch (err) {
setError(err instanceof ApiError ? err.message : "Não foi possível converter o lead.");
} finally {
setSubmitting(false);
}
}
return (
<div>
<Breadcrumb items={[{ label: "Leads", href: "/leads" }, { label: "Converter em oportunidade" }]} />
<PageHeader
title="Converter lead em oportunidade"
description={lead ? `Lead: ${lead.contactName}` : undefined}
/>
{error ? (
<Alert variant="error" className="mb-4">
{error}
</Alert>
) : null}
<Card>
{loading ? (
<p className="text-sm text-default">Carregando lead…</p>
) : (
<form className="grid grid-cols-1 gap-4 sm:grid-cols-2" onSubmit={handleSubmit}>
<Input
label="Título da oportunidade"
className="sm:col-span-2"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
autoFocus
/>
<Select
label="Estágio"
options={STAGE_OPTIONS}
value={stage}
onChange={(e) => setStage(e.target.value as OpportunityStage)}
/>
<Input
label="Probabilidade (%)"
type="number"
min={0}
max={100}
value={probability}
onChange={(e) => setProbability(e.target.value)}
/>
<div className="sm:col-span-2 flex flex-col gap-1">
<label className="text-sm font-semibold text-gray-900">Notas</label>
<textarea
className="w-full rounded-md border border-border-color bg-white px-3 py-2.5 text-sm text-title focus:outline-none focus:ring-2 focus:ring-primary/30"
rows={4}
value={notes}
onChange={(e) => setNotes(e.target.value)}
/>
</div>
<div className="sm:col-span-2 flex items-center gap-2 pt-2">
<Button type="submit" loading={submitting}>
Converter
</Button>
<Button type="button" variant="secondary" onClick={() => navigate("/leads")}>
Cancelar
</Button>
</div>
</form>
)}
</Card>
</div>
);
}

View File

@@ -0,0 +1,35 @@
import { Link, useLocation } from "react-router-dom";
/**
* Only two top-level nav slots were allocated for CRM ("CRM" → /leads,
* per AuthenticatedLayout.tsx) but the module has two screens (Leads,
* Oportunidades) — this small in-page toggle (visual reference: the
* list/grid view switch in `.tmp/theme-reference/crm/leads.tsx`,
* `inline-flex bg-white border border-border-color rounded-md p-1`) lets
* users move between them without a second top-level nav entry.
*/
export function CrmSubNav() {
const location = useLocation();
const onLeads = location.pathname.startsWith("/leads");
return (
<div className="mb-4 inline-flex rounded-md border border-border-color bg-white p-1">
<Link
to="/leads"
className={`rounded px-3 py-1.5 text-sm font-semibold ${
onLeads ? "bg-dark text-white" : "text-default hover:bg-light"
}`}
>
Leads
</Link>
<Link
to="/opportunities"
className={`rounded px-3 py-1.5 text-sm font-semibold ${
!onLeads ? "bg-dark text-white" : "text-default hover:bg-light"
}`}
>
Oportunidades
</Link>
</div>
);
}

View File

@@ -0,0 +1,144 @@
import { useEffect, useState } from "react";
import type { FormEvent } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Alert, Button, Card, Input, PageHeader, Select } from "@eden/ui";
import { useAuth } from "../../lib/auth/AuthContext.js";
import { ApiError } from "../../lib/api/http.js";
import { createLead, getLead, LEAD_STATUSES, updateLead } from "../../lib/api/leads.js";
import type { LeadStatus } from "../../lib/api/leads.js";
import { Breadcrumb } from "../../components/Breadcrumb.js";
const STATUS_LABEL: Record<string, string> = {
new: "Novo",
contacted: "Contatado",
qualified: "Qualificado",
converted: "Convertido",
discarded: "Descartado",
};
const STATUS_OPTIONS = LEAD_STATUSES.map((status) => ({ value: status, label: STATUS_LABEL[status] ?? status }));
export function LeadFormPage() {
const { id } = useParams<{ id: string }>();
const isEdit = Boolean(id);
const { accessToken } = useAuth();
const navigate = useNavigate();
const [source, setSource] = useState("");
const [contactName, setContactName] = useState("");
const [contactEmail, setContactEmail] = useState("");
const [contactPhone, setContactPhone] = useState("");
const [companyName, setCompanyName] = useState("");
const [status, setStatus] = useState<LeadStatus>("new");
const [notes, setNotes] = useState("");
const [loading, setLoading] = useState(isEdit);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!id || !accessToken) return;
setLoading(true);
getLead(id, accessToken)
.then((lead) => {
setSource(lead.source ?? "");
setContactName(lead.contactName);
setContactEmail(lead.contactEmail ?? "");
setContactPhone(lead.contactPhone ?? "");
setCompanyName(lead.companyName ?? "");
setStatus((lead.status as LeadStatus) ?? "new");
setNotes(lead.notes ?? "");
})
.catch((err) => setError(err instanceof ApiError ? err.message : "Não foi possível carregar o lead."))
.finally(() => setLoading(false));
}, [id, accessToken]);
async function handleSubmit(event: FormEvent) {
event.preventDefault();
if (!accessToken) return;
setError(null);
setSubmitting(true);
try {
const input = {
source: source || undefined,
contactName,
contactEmail: contactEmail || undefined,
contactPhone: contactPhone || undefined,
companyName: companyName || undefined,
status,
notes: notes || undefined,
};
if (isEdit && id) {
await updateLead(id, input, accessToken);
} else {
await createLead(input, accessToken);
}
navigate("/leads", { replace: true });
} catch (err) {
setError(err instanceof ApiError ? err.message : "Não foi possível salvar o lead.");
} finally {
setSubmitting(false);
}
}
return (
<div>
<Breadcrumb items={[{ label: "Leads", href: "/leads" }, { label: isEdit ? "Editar lead" : "Novo lead" }]} />
<PageHeader title={isEdit ? "Editar lead" : "Novo lead"} />
{error ? (
<Alert variant="error" className="mb-4">
{error}
</Alert>
) : null}
<Card>
{loading ? (
<p className="text-sm text-default">Carregando lead…</p>
) : (
<form className="grid grid-cols-1 gap-4 sm:grid-cols-2" onSubmit={handleSubmit}>
<Input
label="Nome de contato"
value={contactName}
onChange={(e) => setContactName(e.target.value)}
required
autoFocus
/>
<Input label="Empresa" value={companyName} onChange={(e) => setCompanyName(e.target.value)} />
<Input
label="E-mail"
type="email"
value={contactEmail}
onChange={(e) => setContactEmail(e.target.value)}
/>
<Input label="Telefone" value={contactPhone} onChange={(e) => setContactPhone(e.target.value)} />
<Input label="Origem" value={source} onChange={(e) => setSource(e.target.value)} hint="Ex.: Site, indicação, campanha" />
<Select
label="Status"
options={STATUS_OPTIONS}
value={status}
onChange={(e) => setStatus(e.target.value as LeadStatus)}
/>
<div className="sm:col-span-2 flex flex-col gap-1">
<label className="text-sm font-semibold text-gray-900">Notas</label>
<textarea
className="w-full rounded-md border border-border-color bg-white px-3 py-2.5 text-sm text-title focus:outline-none focus:ring-2 focus:ring-primary/30"
rows={4}
value={notes}
onChange={(e) => setNotes(e.target.value)}
/>
</div>
<div className="sm:col-span-2 flex items-center gap-2 pt-2">
<Button type="submit" loading={submitting}>
{isEdit ? "Salvar alterações" : "Criar lead"}
</Button>
<Button type="button" variant="secondary" onClick={() => navigate("/leads")}>
Cancelar
</Button>
</div>
</form>
)}
</Card>
</div>
);
}

View File

@@ -0,0 +1,137 @@
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { Alert, Badge, Card, PageHeader } from "@eden/ui";
import type { BadgeVariant } from "@eden/ui";
import { useAuth } from "../../lib/auth/AuthContext.js";
import { ApiError } from "../../lib/api/http.js";
import { listLeads } from "../../lib/api/leads.js";
import type { Lead } from "../../lib/api/leads.js";
import { PRIMARY_LINK_BUTTON_CLASS } from "../../components/linkButtonClasses.js";
import { CrmSubNav } from "./CrmSubNav.js";
/**
* Layout reference: DreamsERP theme `.tmp/theme-reference/crm/leads.tsx`
* (list header with title + "Create" action, flush data table inside a
* bordered white card, colored status badges) — rebuilt here with
* `@eden/ui` (`PageHeader`, `Card`, `Badge`, `Button`) + plain Tailwind
* instead of the theme's antd Datatable/react-select, which are outside
* our stack.
*/
const STATUS_LABEL: Record<string, string> = {
new: "Novo",
contacted: "Contatado",
qualified: "Qualificado",
converted: "Convertido",
discarded: "Descartado",
};
const STATUS_VARIANT: Record<string, BadgeVariant> = {
new: "info",
contacted: "warning",
qualified: "primary",
converted: "success",
discarded: "danger",
};
export function LeadsListPage() {
const { accessToken } = useAuth();
const [leads, setLeads] = useState<Lead[] | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!accessToken) return;
let cancelled = false;
setError(null);
listLeads(accessToken)
.then((result) => {
if (!cancelled) setLeads(result);
})
.catch((err) => {
if (!cancelled) setError(err instanceof ApiError ? err.message : "Não foi possível carregar os leads.");
});
return () => {
cancelled = true;
};
}, [accessToken]);
return (
<div>
<CrmSubNav />
<PageHeader
title="Leads"
description="Prospecção comercial — CRM"
actions={
<Link to="/leads/new" className={PRIMARY_LINK_BUTTON_CLASS}>
Novo lead
</Link>
}
/>
{error ? (
<Alert variant="error" className="mb-4">
{error}
</Alert>
) : null}
<Card bodyClassName="p-0">
{leads === null && !error ? (
<p className="p-5 text-sm text-default">Carregando leads…</p>
) : leads && leads.length === 0 ? (
<p className="p-5 text-sm text-default">Nenhum lead cadastrado ainda.</p>
) : leads ? (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border-color text-left">
<th className="px-5 py-3 font-semibold text-title">Contato</th>
<th className="px-5 py-3 font-semibold text-title">Empresa</th>
<th className="px-5 py-3 font-semibold text-title">Status</th>
<th className="px-5 py-3 font-semibold text-title">Ações</th>
</tr>
</thead>
<tbody>
{leads.map((lead) => {
const canConvert = lead.status !== "converted" && lead.status !== "discarded";
return (
<tr key={lead.id} className="border-b border-border-color last:border-0 hover:bg-light">
<td className="px-5 py-3">
<Link to={`/leads/${lead.id}/edit`} className="font-semibold text-title hover:text-primary">
{lead.contactName}
</Link>
{lead.contactEmail ? <p className="text-xs text-default">{lead.contactEmail}</p> : null}
</td>
<td className="px-5 py-3 text-default">{lead.companyName ?? "—"}</td>
<td className="px-5 py-3">
<Badge variant={STATUS_VARIANT[lead.status] ?? "neutral"}>
{STATUS_LABEL[lead.status] ?? lead.status}
</Badge>
</td>
<td className="px-5 py-3">
<div className="flex flex-wrap items-center gap-2">
<Link
to={`/leads/${lead.id}/edit`}
className="text-xs font-semibold text-default hover:text-primary"
>
Editar
</Link>
{canConvert ? (
<Link
to={`/leads/${lead.id}/convert`}
className="rounded-md border border-border-color px-2 py-1 text-xs font-semibold text-title hover:bg-light"
>
Converter em oportunidade
</Link>
) : null}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
) : null}
</Card>
</div>
);
}

View File

@@ -0,0 +1,130 @@
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { Alert, Badge, Card, PageHeader } from "@eden/ui";
import type { BadgeVariant } from "@eden/ui";
import { useAuth } from "../../lib/auth/AuthContext.js";
import { ApiError } from "../../lib/api/http.js";
import { listOpportunities } from "../../lib/api/opportunities.js";
import type { Opportunity } from "../../lib/api/opportunities.js";
import { PRIMARY_LINK_BUTTON_CLASS } from "../../components/linkButtonClasses.js";
import { CrmSubNav } from "./CrmSubNav.js";
/**
* Layout reference: DreamsERP theme `.tmp/theme-reference/crm/deals.tsx`
* (kanban-oriented in the theme; kept as a flat table here for MVP scope,
* same header/card/table language as `LeadsListPage`).
*/
const STAGE_LABEL: Record<string, string> = {
prospecting: "Prospecção",
qualification: "Qualificação",
proposal: "Proposta",
negotiation: "Negociação",
won: "Ganha",
lost: "Perdida",
};
const STAGE_VARIANT: Record<string, BadgeVariant> = {
prospecting: "info",
qualification: "primary",
proposal: "warning",
negotiation: "secondary",
won: "success",
lost: "danger",
};
export function OpportunitiesListPage() {
const { accessToken } = useAuth();
const [opportunities, setOpportunities] = useState<Opportunity[] | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!accessToken) return;
let cancelled = false;
setError(null);
listOpportunities(accessToken)
.then((result) => {
if (!cancelled) setOpportunities(result);
})
.catch((err) => {
if (!cancelled)
setError(err instanceof ApiError ? err.message : "Não foi possível carregar as oportunidades.");
});
return () => {
cancelled = true;
};
}, [accessToken]);
return (
<div>
<CrmSubNav />
<PageHeader
title="Oportunidades"
description="Funil comercial — CRM"
actions={
<Link to="/opportunities/new" className={PRIMARY_LINK_BUTTON_CLASS}>
Nova oportunidade
</Link>
}
/>
{error ? (
<Alert variant="error" className="mb-4">
{error}
</Alert>
) : null}
<Card bodyClassName="p-0">
{opportunities === null && !error ? (
<p className="p-5 text-sm text-default">Carregando oportunidades…</p>
) : opportunities && opportunities.length === 0 ? (
<p className="p-5 text-sm text-default">Nenhuma oportunidade cadastrada ainda.</p>
) : opportunities ? (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border-color text-left">
<th className="px-5 py-3 font-semibold text-title">Título</th>
<th className="px-5 py-3 font-semibold text-title">Estágio</th>
<th className="px-5 py-3 font-semibold text-title">Probabilidade</th>
<th className="px-5 py-3 font-semibold text-title">Dono</th>
<th className="px-5 py-3 font-semibold text-title">Ações</th>
</tr>
</thead>
<tbody>
{opportunities.map((opportunity) => (
<tr key={opportunity.id} className="border-b border-border-color last:border-0 hover:bg-light">
<td className="px-5 py-3">
<Link
to={`/opportunities/${opportunity.id}/edit`}
className="font-semibold text-title hover:text-primary"
>
{opportunity.title}
</Link>
</td>
<td className="px-5 py-3">
<Badge variant={STAGE_VARIANT[opportunity.stage] ?? "neutral"}>
{STAGE_LABEL[opportunity.stage] ?? opportunity.stage}
</Badge>
</td>
<td className="px-5 py-3 text-default">
{opportunity.probability != null ? `${opportunity.probability}%` : "—"}
</td>
<td className="px-5 py-3 text-default">{opportunity.ownerId ?? "—"}</td>
<td className="px-5 py-3">
<Link
to={`/opportunities/${opportunity.id}/edit`}
className="text-xs font-semibold text-default hover:text-primary"
>
Editar
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : null}
</Card>
</div>
);
}

View File

@@ -0,0 +1,157 @@
import { useEffect, useState } from "react";
import type { FormEvent } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Alert, Button, Card, Input, PageHeader, Select } from "@eden/ui";
import { useAuth } from "../../lib/auth/AuthContext.js";
import { ApiError } from "../../lib/api/http.js";
import {
createOpportunity,
getOpportunity,
OPPORTUNITY_STAGES,
updateOpportunity,
} from "../../lib/api/opportunities.js";
import type { OpportunityStage } from "../../lib/api/opportunities.js";
import { Breadcrumb } from "../../components/Breadcrumb.js";
const STAGE_LABEL: Record<OpportunityStage, string> = {
prospecting: "Prospecção",
qualification: "Qualificação",
proposal: "Proposta",
negotiation: "Negociação",
won: "Ganha",
lost: "Perdida",
};
const STAGE_OPTIONS = OPPORTUNITY_STAGES.map((stage) => ({ value: stage, label: STAGE_LABEL[stage] }));
export function OpportunityFormPage() {
const { id } = useParams<{ id: string }>();
const isEdit = Boolean(id);
const { accessToken } = useAuth();
const navigate = useNavigate();
const [title, setTitle] = useState("");
const [stage, setStage] = useState<OpportunityStage>("prospecting");
const [probability, setProbability] = useState("");
const [lossReason, setLossReason] = useState("");
const [notes, setNotes] = useState("");
const [loading, setLoading] = useState(isEdit);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!id || !accessToken) return;
setLoading(true);
getOpportunity(id, accessToken)
.then((opportunity) => {
setTitle(opportunity.title);
setStage((opportunity.stage as OpportunityStage) ?? "prospecting");
setProbability(opportunity.probability != null ? String(opportunity.probability) : "");
setLossReason(opportunity.lossReason ?? "");
setNotes(opportunity.notes ?? "");
})
.catch((err) => setError(err instanceof ApiError ? err.message : "Não foi possível carregar a oportunidade."))
.finally(() => setLoading(false));
}, [id, accessToken]);
async function handleSubmit(event: FormEvent) {
event.preventDefault();
if (!accessToken) return;
setError(null);
setSubmitting(true);
try {
const input = {
title,
stage,
probability: probability ? Number(probability) : undefined,
lossReason: stage === "lost" && lossReason ? lossReason : undefined,
notes: notes || undefined,
};
if (isEdit && id) {
await updateOpportunity(id, input, accessToken);
} else {
await createOpportunity(input, accessToken);
}
navigate("/opportunities", { replace: true });
} catch (err) {
setError(err instanceof ApiError ? err.message : "Não foi possível salvar a oportunidade.");
} finally {
setSubmitting(false);
}
}
return (
<div>
<Breadcrumb
items={[
{ label: "Oportunidades", href: "/opportunities" },
{ label: isEdit ? "Editar oportunidade" : "Nova oportunidade" },
]}
/>
<PageHeader title={isEdit ? "Editar oportunidade" : "Nova oportunidade"} />
{error ? (
<Alert variant="error" className="mb-4">
{error}
</Alert>
) : null}
<Card>
{loading ? (
<p className="text-sm text-default">Carregando oportunidade…</p>
) : (
<form className="grid grid-cols-1 gap-4 sm:grid-cols-2" onSubmit={handleSubmit}>
<Input
label="Título"
className="sm:col-span-2"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
autoFocus
/>
<Select
label="Estágio"
options={STAGE_OPTIONS}
value={stage}
onChange={(e) => setStage(e.target.value as OpportunityStage)}
/>
<Input
label="Probabilidade (%)"
type="number"
min={0}
max={100}
value={probability}
onChange={(e) => setProbability(e.target.value)}
/>
{stage === "lost" ? (
<Input
label="Motivo da perda"
className="sm:col-span-2"
value={lossReason}
onChange={(e) => setLossReason(e.target.value)}
/>
) : null}
<div className="sm:col-span-2 flex flex-col gap-1">
<label className="text-sm font-semibold text-gray-900">Notas</label>
<textarea
className="w-full rounded-md border border-border-color bg-white px-3 py-2.5 text-sm text-title focus:outline-none focus:ring-2 focus:ring-primary/30"
rows={4}
value={notes}
onChange={(e) => setNotes(e.target.value)}
/>
</div>
<div className="sm:col-span-2 flex items-center gap-2 pt-2">
<Button type="submit" loading={submitting}>
{isEdit ? "Salvar alterações" : "Criar oportunidade"}
</Button>
<Button type="button" variant="secondary" onClick={() => navigate("/opportunities")}>
Cancelar
</Button>
</div>
</form>
)}
</Card>
</div>
);
}