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

@@ -4,10 +4,12 @@ import { cn } from "../utils/cn.js";
export interface CardProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
title?: ReactNode;
actions?: ReactNode;
/** Overrides the default `p-5` on the content wrapper — e.g. `"p-0"` for a flush data table (theme source: sales/salesQuotes.tsx uses `p-4 pb-0` around its table). */
bodyClassName?: string;
}
/** Theme source (auth-pages/login.tsx): `bg-white border border-border-color rounded-md shadow-sm`. */
export function Card({ title, actions, className, children, ...rest }: CardProps) {
export function Card({ title, actions, className, bodyClassName, children, ...rest }: CardProps) {
return (
<div className={cn("rounded-md border border-border-color bg-white shadow-sm", className)} {...rest}>
{title || actions ? (
@@ -16,7 +18,7 @@ export function Card({ title, actions, className, children, ...rest }: CardProps
{actions ? <div className="flex items-center gap-2">{actions}</div> : null}
</div>
) : null}
<div className="p-5">{children}</div>
<div className={bodyClassName ?? "p-5"}>{children}</div>
</div>
);
}

View File

@@ -0,0 +1,93 @@
import { forwardRef, useId } from "react";
import type { SelectHTMLAttributes } from "react";
import { cn } from "../utils/cn.js";
export interface SelectOption {
value: string;
label: string;
}
export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "children"> {
label?: string;
/** Validation/server error message — shown below the field, field border turns danger. */
error?: string;
/** Helper text shown below the field when there's no error. */
hint?: string;
options: SelectOption[];
/** Rendered as a disabled, selected-by-default first option (e.g. "Selecione..."). */
placeholder?: string;
}
/**
* Native `<select>` styled to match `Input` (packages/ui/src/components/Input.tsx)
* — same border/padding/focus-ring language as the rest of the Design
* System. Derived from the DreamsERP theme's form field sizing
* (`.tmp/theme-reference/sales/addSalesQuotes.tsx` etc. use a
* `react-select`-based CommonSelect for the same slots, out of our stack —
* this is a plain, dependency-free equivalent with the same visual sizing).
*/
export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select(
{ label, error, hint, id, className, options, placeholder, ...rest },
ref,
) {
const generatedId = useId();
const selectId = id ?? generatedId;
return (
<div className="flex flex-col gap-1">
{label ? (
<label htmlFor={selectId} className="text-sm font-semibold text-gray-900">
{label}
</label>
) : null}
<div className="relative">
<select
ref={ref}
id={selectId}
aria-invalid={error ? true : undefined}
aria-describedby={error ? `${selectId}-error` : hint ? `${selectId}-hint` : undefined}
className={cn(
"w-full appearance-none rounded-md border bg-white px-3 py-2.5 pe-9 text-sm text-title focus:outline-none focus:ring-2 focus:ring-primary/30",
error ? "border-danger" : "border-border-color",
className,
)}
{...rest}
>
{placeholder ? (
<option value="" disabled hidden>
{placeholder}
</option>
) : null}
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<svg
aria-hidden="true"
viewBox="0 0 20 20"
fill="none"
className="pointer-events-none absolute end-3 top-1/2 size-3.5 -translate-y-1/2 text-default"
>
<path
d="M5 7.5L10 12.5L15 7.5"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
{error ? (
<p id={`${selectId}-error`} className="text-xs text-danger">
{error}
</p>
) : hint ? (
<p id={`${selectId}-hint`} className="text-xs text-default">
{hint}
</p>
) : null}
</div>
);
});

View File

@@ -0,0 +1,64 @@
import type { ReactNode } from "react";
import { cn } from "../utils/cn.js";
export interface TableColumn<T> {
key: string;
header: ReactNode;
render: (row: T) => ReactNode;
/** Applied to both the header and body cells — e.g. `"text-right"` for numeric columns. */
className?: string;
}
export interface TableProps<T> {
columns: Array<TableColumn<T>>;
data: T[];
rowKey: (row: T) => string;
/** Shown instead of the table body when `data` is empty (and not loading). */
emptyMessage?: ReactNode;
}
/**
* Plain data table styled to match the DreamsERP theme's list screens
* (react/src/pages/inventory/products.tsx et al.) without pulling in the
* theme's antd/react-window dependencies — just a semantic `<table>`.
*/
export function Table<T>({ columns, data, rowKey, emptyMessage = "Nenhum registro encontrado." }: TableProps<T>) {
return (
<div className="overflow-x-auto">
<table className="w-full border-collapse text-left">
<thead>
<tr className="border-b border-border-color">
{columns.map((column) => (
<th
key={column.key}
scope="col"
className={cn("whitespace-nowrap px-4 py-3 text-xs font-semibold uppercase text-default", column.className)}
>
{column.header}
</th>
))}
</tr>
</thead>
<tbody>
{data.length === 0 ? (
<tr>
<td colSpan={columns.length} className="px-4 py-8 text-center text-sm text-default">
{emptyMessage}
</td>
</tr>
) : (
data.map((row) => (
<tr key={rowKey(row)} className="border-b border-border-color last:border-0 hover:bg-light">
{columns.map((column) => (
<td key={column.key} className={cn("px-4 py-3 text-sm text-title", column.className)}>
{column.render(row)}
</td>
))}
</tr>
))
)}
</tbody>
</table>
</div>
);
}

View File

@@ -13,6 +13,12 @@ export type { ButtonProps, ButtonVariant, ButtonSize } from "./components/Button
export { Input } from "./components/Input.js";
export type { InputProps } from "./components/Input.js";
export { Select } from "./components/Select.js";
export type { SelectProps, SelectOption } from "./components/Select.js";
export { Table } from "./components/Table.js";
export type { TableProps, TableColumn } from "./components/Table.js";
export { Card } from "./components/Card.js";
export type { CardProps } from "./components/Card.js";