diff --git a/README.md b/README.md index 567da13..8b332ba 100644 --- a/README.md +++ b/README.md @@ -1,104 +1,26 @@ -# SaveInMed — Monorepo +# SaveInMed -> Marketplace B2B para o setor farmacêutico brasileiro. -> Conecta distribuidoras, farmácias e operadores logísticos em uma plataforma única. +Monorepo do SaveInMed. ---- +## Estrutura -## Estrutura do repositório +- `backend/`: API principal em Go +- `frontend/`: marketplace web +- `backoffice/`: API administrativa +- `website/`: site institucional +- `docs/`: documentacao tecnica -``` -saveinmed/ -├── backend/ # API principal (Go · Clean Architecture) -├── frontend/ # Marketplace web (React + Vite + Tailwind) -├── backoffice/ # API administrativa (NestJS · Prisma) -├── seeder-api/ # Seeder de catálogo de produtos -├── website/ # Site institucional -└── docs/ # Documentação técnica centralizada -``` +## Desenvolvimento -### `backend/` -API REST em Go seguindo Clean Architecture: -- **`internal/domain/`** — modelos, regras e contratos de domínio -- **`internal/usecase/`** — casos de uso por domínio (`company_usecase.go`, `order_usecase.go`, …) -- **`internal/http/handler/`** — handlers HTTP (um arquivo por recurso) -- **`internal/repository/postgres/`** — implementações de repositório + migrações SQL -- **`internal/infrastructure/`** — integrações externas: `mapbox/`, `payments/`, `notifications/` -- **`cmd/api/`** — entrypoint da API REST -- **`cmd/seeder/`** — seed de dados +- `main`: producao +- `hml`: homologacao +- `dev`: desenvolvimento -### `frontend/` -Marketplace React com Vite. Páginas organizadas por perfil de usuário: -- **`src/pages/auth/`** — Login, registro, recuperação de senha -- **`src/pages/marketplace/`** — busca, carrinho, checkout, pedidos -- **`src/pages/dashboard/admin/`** — painel admin (consome a API do `backoffice`) -- **`src/pages/dashboard/seller/`** — painel do vendedor/distribuidora -- **`src/pages/dashboard/employee/`** — painel do colaborador -- **`src/pages/dashboard/delivery/`** — painel do entregador -- **`src/components/`** — componentes reutilizáveis -- **`src/stores/`** — Zustand (carrinho, filtros, UI global) -- **`src/context/`** — Context API (auth + tema) -- **`src/services/`** — clientes HTTP por domínio +## Backend -### `backoffice/` -API NestJS para operações administrativas internas: KYC, auditoria, disputas, relatórios, detecção de fraude. -Conecta ao mesmo banco de dados do `backend` via Prisma. -O painel React em `frontend/src/pages/dashboard/admin/` é o **frontend desta API**. - ---- - -## Gerenciamento de estado (frontend) - -| Estado | Onde | Justificativa | -|--------|------|---------------| -| Autenticação (token JWT, user) | Context API (`AuthContext`) | Semântica de provider; mudanças raras | -| Tema claro/escuro | Context API (`ThemeContext`) | Idem | -| Carrinho de compras | Zustand (`cartStore`) | Atualizações frequentes; sem re-render do provider raiz | -| Filtros de busca persistentes | Zustand / `usePersistentFilters` | Sobrevive a navegações | -| Estado de UI local (modais, loaders) | `useState` local | Não precisa ser compartilhado | - -> **Regra:** Use Context apenas para auth/tema. Para estado de domínio ou UI global, use Zustand. - ---- - -## Comandos úteis +Para subir a API localmente: ```bash -# Backend Go (porta 8214 por padrão) -cd backend && go run ./cmd/api - -# Frontend marketplace -cd frontend && pnpm dev - -# Backoffice API -cd backoffice && pnpm start:dev - -# Aplicar migrações Go -cd backend && go run ./cmd/apply_migration - -# Seed manual -cd backend && go run ./cmd/seeder +cd backend +go run ./cmd/api ``` - -Swagger disponível em `http://localhost:8214/swagger/index.html`. - ---- - -## Convenções - -- **Commits semânticos:** `feat:`, `fix:`, `refactor:`, `docs:`, `chore:` -- **Branches:** `main` = produção · `develop` = staging · `feature/*` = funcionalidades -- **PRs:** obrigatório para `main` com ao menos 1 revisor - ---- - -## Documentação técnica - -| Arquivo | Conteúdo | -|---------|----------| -| [docs/architecture.md](docs/architecture.md) | Visão geral da arquitetura e decisões de design | -| [docs/BACKEND.md](docs/BACKEND.md) | Backend Go — detalhes, handlers, domínios | -| [docs/BACKOFFICE.md](docs/BACKOFFICE.md) | NestJS backoffice — módulos e responsabilidades | -| [docs/DATABASE.md](docs/DATABASE.md) | Modelo de dados e migrações | -| [docs/ROADMAP.md](docs/ROADMAP.md) | Roadmap de produto | -| [STATUS_REPORT_SAVEINMED.md](STATUS_REPORT_SAVEINMED.md) | Status detalhado de cada funcionalidade | diff --git a/backend/.env b/backend/.env index eb0eaf7..2d42ba3 100644 --- a/backend/.env +++ b/backend/.env @@ -41,5 +41,5 @@ SWAGGER_SCHEMES=http,https # Testing (Optional) # SKIP_DB_TEST=1 -# API Configuration -BACKEND_HOST=https://api.saveinmed.com.br \ No newline at end of file +# API Configuration (production - commented for dev) +# BACKEND_HOST=https://api.saveinmed.com.br diff --git a/backend/internal/server/server.go b/backend/internal/server/server.go index e779611..80aca6b 100644 --- a/backend/internal/server/server.go +++ b/backend/internal/server/server.go @@ -109,7 +109,8 @@ func New(cfg config.Config) (*Server, error) { mux.Handle("GET /api/v1/marketplace/records", chain(http.HandlerFunc(h.ListMarketplaceRecords), middleware.Logger, middleware.Gzip)) mux.Handle("GET /api/v1/laboratorios", chain(http.HandlerFunc(h.ListManufacturers), middleware.Logger, middleware.Gzip)) mux.Handle("GET /api/v1/categorias", chain(http.HandlerFunc(h.ListCategories), middleware.Logger, middleware.Gzip)) - mux.Handle("GET /api/v1/produtos-catalogo", chain(http.HandlerFunc(h.ListProducts), middleware.Logger, middleware.Gzip)) // Alias + mux.Handle("GET /api/v1/produtos-catalogo", chain(http.HandlerFunc(h.ListProducts), middleware.Logger, middleware.Gzip)) // Alias + mux.Handle("POST /api/v1/produtos-catalogo", chain(http.HandlerFunc(h.CreateProduct), middleware.Logger, middleware.Gzip, productManagers)) // Alias for frontend mux.Handle("GET /api/v1/produtos-catalogo/codigo-ean/{ean}", chain(http.HandlerFunc(h.GetProductByEAN), middleware.Logger, middleware.Gzip)) mux.Handle("GET /api/v1/inventory", chain(http.HandlerFunc(h.ListInventory), middleware.Logger, middleware.Gzip, auth)) diff --git a/docker-compose.yml b/docker-compose.yml index 1262073..e738ad3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,6 @@ services: POSTGRES_PASSWORD: "123" POSTGRES_DB: saveinmed ports: - # Porta 55432 no host → 5432 no container (alinhado com backend/.env) - "55432:5432" volumes: - saveinmed_pgdata:/var/lib/postgresql/data diff --git a/frontend/src/components/CadastroProdutoWizard.tsx b/frontend/src/components/CadastroProdutoWizard.tsx index 8e19776..ebe42df 100644 --- a/frontend/src/components/CadastroProdutoWizard.tsx +++ b/frontend/src/components/CadastroProdutoWizard.tsx @@ -679,16 +679,17 @@ const CadastroProdutoWizard: React.FC = () => { const data = await response.json(); - // A API pode retornar { items: [...] } ou direto um array - let produtos = data.items || data.data || data || []; + // A API retorna { products: [...], total, page, page_size } + let produtos = data.products || data.items || data.data || data || []; // Se for array, filtrar localmente por nome if (Array.isArray(produtos)) { const nomeMinusculo = nome.toLowerCase(); - const produtosFiltrados = produtos.filter(produto => - produto.nome && produto.nome.toLowerCase().includes(nomeMinusculo) - ); + const produtosFiltrados = produtos.filter((produto: any) => { + const prodNome = produto.nome || produto.name || ""; + return prodNome.toLowerCase().includes(nomeMinusculo); + }); if (produtosFiltrados.length > 0) { @@ -812,55 +813,22 @@ const CadastroProdutoWizard: React.FC = () => { return; } - setCriandoLaboratorio(true); + // Laboratórios no backend são apenas strings na coluna manufacturer + // Não é necessário criar via POST - basta usar o texto digitado + const novoLab = { + id: laboratorioNome.trim(), + nome: laboratorioNome.trim(), + }; - try { - const token = localStorage.getItem('access_token'); - if (!token) { - toast.error("Token de acesso não encontrado"); - setCriandoLaboratorio(false); - return; - } + // Adicionar à lista local de laboratórios para exibição + setLaboratorios((prev) => [...prev, novoLab]); + // Selecionar o novo laboratório + setStepOne((prev) => ({ ...prev, laboratorio: novoLab.id })); + setShowLaboratorioSuggestions(false); + setLaboratoriosSugeridos([]); - const response = await fetch(`${process.env.NEXT_PUBLIC_BFF_API_URL}/laboratorios`, { - method: 'POST', - headers: { - 'accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - body: JSON.stringify({ - nome: laboratorioNome.trim() - }), - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - toast.error(errorData.message || "Erro ao criar laboratório"); - setCriandoLaboratorio(false); - return; - } - - const novoLab = await response.json(); - - // Adicionar à lista de laboratórios - setLaboratorios((prev) => [...prev, novoLab]); - - // Selecionar o novo laboratório - const labId = novoLab.$id || novoLab.id; - setStepOne((prev) => ({ ...prev, laboratorio: labId })); - setShowLaboratorioSuggestions(false); - setLaboratoriosSugeridos([]); - - toast.success("Laboratório criado com sucesso!"); - - } catch (error) { - console.error("Erro ao criar laboratório:", error); - toast.error("Erro ao criar laboratório"); - } finally { - setCriandoLaboratorio(false); - } + toast.success("Laboratório adicionado!"); }; const handleSubmitStepOne = async (event: React.FormEvent) => { @@ -887,79 +855,66 @@ const CadastroProdutoWizard: React.FC = () => { return; } - // Primeiro, verificar se precisamos criar um novo laboratório - let laboratorioId = stepOne.laboratorio; + // Resolver nome do laboratório (manufacturer) - labs são strings, não entidades separadas let labNome = ""; - - if (!laboratorioId && laboratorioNome.trim()) { - // Criar novo laboratório - - try { - const labResponse = await fetch(`${process.env.NEXT_PUBLIC_BFF_API_URL}/laboratorios`, { - method: 'POST', - headers: { - 'accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - body: JSON.stringify({ - nome: laboratorioNome.trim() - }), - }); - - if (!labResponse.ok) { - const errorData = await labResponse.json().catch(() => ({})); - toast.error(errorData.message || "Erro ao criar laboratório"); - setSubmitting(false); - return; - } - - const novoLab = await labResponse.json(); - - // Adicionar à lista de laboratórios - setLaboratorios((prev) => [...prev, novoLab]); - - laboratorioId = novoLab.$id || novoLab.id; - labNome = novoLab.nome || novoLab.name || laboratorioNome.trim(); - - // Atualizar estado do stepOne - setStepOne((prev) => ({ ...prev, laboratorio: laboratorioId })); - - } catch (error) { - console.error("Erro ao criar laboratório:", error); - toast.error("Erro ao criar laboratório"); - setSubmitting(false); - return; - } - } else { - // Buscar nome do laboratório existente - const labSelecionado = laboratorios.find(lab => (lab.$id || lab.id) === laboratorioId); - labNome = labSelecionado?.nome || labSelecionado?.name || ''; + if (laboratorioNome.trim()) { + // Usuário digitou um nome de laboratório (novo ou existente) + labNome = laboratorioNome.trim(); + } else if (stepOne.laboratorio) { + // Laboratório selecionado da lista + const labSelecionado = laboratorios.find(lab => (lab.$id || lab.id || lab.nome) === stepOne.laboratorio); + labNome = labSelecionado?.nome || labSelecionado?.name || stepOne.laboratorio; } - // Buscar os nomes de categoria - const catSelecionada = categorias.find(cat => (cat.$id || cat.id) === stepOne.categoria); - const catNome = catSelecionada?.nome || catSelecionada?.name || ''; + // Resolver nome da categoria + let catNome = ""; + if (stepOne.categoria) { + const catSelecionada = categorias.find(cat => (cat.$id || cat.id || cat.nome) === stepOne.categoria); + catNome = catSelecionada?.nome || catSelecionada?.name || stepOne.categoria; + } - const payload = { - documentId: "unique()", - data: { - codigo_ean: stepOne.codigo_ean.trim(), - codigo_interno: stepOne.codigo_interno.trim() || stepOne.codigo_ean.trim(), - nome: stepOne.nome.trim(), - descricao: stepOne.descricao.trim() || "Descrição do produto", - // Usar valores padrão para campos ocultos - preco_base: Number(stepOne.preco_base) || 0, - preco_fabrica: Number(stepOne.preco_fabrica) || 0, - pmc: Number(stepOne.pmc) || 0, - desconto_comercial: Number(stepOne.desconto_comercial) || 10, - valor_substituicao_tributaria: Number(stepOne.valor_substituicao_tributaria) || 5.00, - preco_nf: Number(stepOne.preco_nf) || 0, - lab_nome: labNome, - cat_nome: catNome, + // Buscar empresa_id do usuário logado + let sellerIdStr = await getCurrentUserEmpresaId(); + if (!sellerIdStr) { + sellerIdStr = empresaId || localStorage.getItem('empresaId'); + if (!sellerIdStr) { + const userStr = localStorage.getItem('user'); + if (userStr) { + const user = JSON.parse(userStr); + sellerIdStr = user.company_id || user.empresa_id; + } } + } + + if (!sellerIdStr) { + toast.error("Erro ao obter dados da empresa. Faça login novamente."); + setSubmitting(false); + return; + } + + // Converter valores monetários para centavos (Backend Go espera int64 cents) + const toCents = (val: string | number): number => { + const num = typeof val === 'string' ? parseFloat(val) : val; + return Number.isFinite(num) ? Math.round(num * 100) : 0; }; + // Payload no formato registerProductRequest do Backend Go + const payload = { + seller_id: sellerIdStr, + ean_code: stepOne.codigo_ean.trim(), + name: stepOne.nome.trim(), + description: stepOne.descricao.trim() || "Descrição do produto", + manufacturer: labNome, + category: catNome, + subcategory: stepOne.subcategoria.trim() || "", + price_cents: toCents(stepOne.preco_base), + internal_code: stepOne.codigo_interno.trim() || stepOne.codigo_ean.trim(), + factory_price_cents: toCents(stepOne.preco_fabrica), + pmc_cents: toCents(stepOne.pmc), + commercial_discount_cents: toCents(stepOne.desconto_comercial), + tax_substitution_cents: toCents(stepOne.valor_substituicao_tributaria), + invoice_price_cents: toCents(stepOne.preco_nf), + }; const response = await fetch(`${process.env.NEXT_PUBLIC_BFF_API_URL}/produtos-catalogo`, { method: 'POST', @@ -975,13 +930,13 @@ const CadastroProdutoWizard: React.FC = () => { if (!response.ok) { const errorData = await response.json().catch(() => ({})); - toast.error(errorData.message || "Erro ao criar produto no catálogo"); + toast.error(errorData.error || errorData.message || "Erro ao criar produto no catálogo"); return; } const data = await response.json(); - const catalogoId = data.documentId || data.$id || data.id; + const catalogoId = data.id || data.documentId || data.$id; if (catalogoId) { setReferenciaCatalogoId(catalogoId); diff --git a/frontend/src/utils/authUtils.ts b/frontend/src/utils/authUtils.ts index f6e45d9..15c8e90 100644 --- a/frontend/src/utils/authUtils.ts +++ b/frontend/src/utils/authUtils.ts @@ -67,7 +67,11 @@ export function extractEmpresaId(userData: UserMeResponse | null): string | null return null; } - + // Fallback: verificar company_id direto (Backend Go retorna isso) + const anyData = userData as any; + if (anyData.company_id && typeof anyData.company_id === 'string') { + return anyData.company_id; + } // Verifica se há empresas associadas if (!userData.empresasDados) {