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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
93
packages/ui/src/components/Select.tsx
Normal file
93
packages/ui/src/components/Select.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
64
packages/ui/src/components/Table.tsx
Normal file
64
packages/ui/src/components/Table.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user