refactor(frontend): decouple catalog components and api routes from appwrite stub

This commit is contained in:
Tiago Yamamoto 2026-03-07 08:04:19 -06:00
parent 48a54a616e
commit a1eb0efc72
6 changed files with 213 additions and 334 deletions

View file

@ -1,56 +1,63 @@
// @ts-nocheck
import { NextRequest, NextResponse } from "next/server";
import { databases } from "@/lib/appwrite";
import { Query } from "@/lib/appwrite";
import { NextRequest, NextResponse } from 'next/server';
import { Client, Databases, Query } from 'node-appwrite';
export const runtime = "edge";
export const runtime = 'edge';
const createDatabasesClient = () => {
const apiKey = process.env.APPWRITE_API_KEY;
const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT;
const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID;
if (!apiKey || !endpoint || !projectId) {
throw new Error('Configuração do Appwrite ausente');
}
const client = new Client().setEndpoint(endpoint).setProject(projectId).setKey(apiKey);
return new Databases(client);
};
export async function GET(request: NextRequest) {
console.log("🔍 [API] Debug registro usuario...");
console.log('[API] Debug registro usuario...');
try {
const { searchParams } = new URL(request.url);
const userId = searchParams.get("userId");
const userId = searchParams.get('userId');
if (!userId) {
return NextResponse.json(
{ success: false, error: "userId é obrigatório" },
{ success: false, error: 'userId é obrigatório' },
{ status: 400 }
);
}
const databaseId = process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID!;
const collectionId =
process.env.NEXT_PUBLIC_APPWRITE_COLLECTION_USUARIOS_ID!;
const collectionId = process.env.NEXT_PUBLIC_APPWRITE_COLLECTION_USUARIOS_ID!;
const databases = createDatabasesClient();
console.log("🔍 [API] Buscando usuário com auth-id-appwrite:", userId);
console.log('[API] Buscando usuário com auth-id-appwrite:', userId);
// Buscar usuário pelo auth-id-appwrite
const response = await databases.listDocuments(databaseId, collectionId, [
Query.equal("auth-id-appwrite", userId),
Query.equal('auth-id-appwrite', userId),
Query.limit(1),
]);
if (response.documents.length === 0) {
console.log("❌ [API] Usuário não encontrado");
console.log('[API] Usuário não encontrado');
return NextResponse.json({
success: true,
found: false,
message: "Usuário não encontrado na base de dados",
message: 'Usuário não encontrado na base de dados',
});
}
const usuario = response.documents[0];
const usuario = response.documents[0] as Record<string, any>;
console.log("✅ [API] Usuário encontrado:", usuario.$id);
// Análise detalhada dos dados
const analise = {
id: usuario.$id,
"auth-id-appwrite": usuario["auth-id-appwrite"],
"nome-civil": {
valor: usuario["nome-civil"],
preenchido: !!usuario["nome-civil"],
'auth-id-appwrite': usuario['auth-id-appwrite'],
'nome-civil': {
valor: usuario['nome-civil'],
preenchido: !!usuario['nome-civil'],
},
cpf: {
valor: usuario.cpf,
@ -64,9 +71,7 @@ export async function GET(request: NextRequest) {
valido:
Array.isArray(usuario.enderecos) &&
usuario.enderecos.length > 0 &&
usuario.enderecos.some(
(endereco) => endereco && endereco.trim() !== ""
),
usuario.enderecos.some((endereco: string) => endereco && endereco.trim() !== ''),
},
empresas_array: {
valor: usuario.empresas_array,
@ -80,29 +85,22 @@ export async function GET(request: NextRequest) {
},
};
const dadosFaltantes = [];
const dadosFaltantes: string[] = [];
// Verificar cada campo
if (!analise["nome-civil"].preenchido || !analise.cpf.preenchido) {
dadosFaltantes.push("Dados pessoais");
if (!analise['nome-civil'].preenchido || !analise.cpf.preenchido) {
dadosFaltantes.push('Dados pessoais');
}
if (!analise.enderecos.valido) {
dadosFaltantes.push("Endereço");
dadosFaltantes.push('Endereço');
}
if (!analise.empresas_array.valido) {
dadosFaltantes.push("Empresa");
dadosFaltantes.push('Empresa');
}
const isCompleto = dadosFaltantes.length === 0;
console.log("📋 [API] Análise completa:", {
isCompleto,
dadosFaltantes,
analise,
});
return NextResponse.json({
success: true,
found: true,
@ -112,11 +110,10 @@ export async function GET(request: NextRequest) {
dadosCompletos: usuario,
});
} catch (error) {
console.error("❌ [API] Erro ao debugar registro:", error);
console.error('[API] Erro ao debugar registro:', error);
return NextResponse.json(
{ success: false, error: "Erro ao verificar registro" },
{ success: false, error: 'Erro ao verificar registro' },
{ status: 500 }
);
}
}

View file

@ -1,85 +1,74 @@
// @ts-nocheck
import { NextRequest, NextResponse } from "next/server";
import { databases, Query } from "@/lib/appwrite";
// @ts-nocheck
import { NextRequest, NextResponse } from 'next/server';
import { Client, Databases, Query } from 'node-appwrite';
// Runtime edge para melhor performance
export const runtime = "edge";
export const runtime = 'edge';
const DATABASE_ID = process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID!;
const COLLECTION_CATALOGO =
process.env.NEXT_PUBLIC_APPWRITE_COLLECTION_CATALOGO_PRODUTOS_ID!;
const COLLECTION_CATEGORIAS =
process.env.NEXT_PUBLIC_APPWRITE_COLLECTION_CATEGORIAS_ID!;
const COLLECTION_CATALOGO = process.env.NEXT_PUBLIC_APPWRITE_COLLECTION_CATALOGO_PRODUTOS_ID!;
const COLLECTION_CATEGORIAS = process.env.NEXT_PUBLIC_APPWRITE_COLLECTION_CATEGORIAS_ID!;
const createDatabasesClient = () => {
const apiKey = process.env.APPWRITE_API_KEY;
const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT;
const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID;
if (!apiKey || !endpoint || !projectId) {
throw new Error('Configuração do Appwrite ausente');
}
const client = new Client().setEndpoint(endpoint).setProject(projectId).setKey(apiKey);
return new Databases(client);
};
// Mapeamento de produtos para categorias
const mapeamentoCategorias: { [key: string]: string } = {
dipirona: "RX",
cetaphil: "Dermo",
sabonete: "Dermo",
dermo: "Dermo",
vitamina: "Nutrição",
suplemento: "Nutrição",
generico: "Genérico",
diabetes: "Diabetes",
insulina: "Diabetes",
medicamento: "RX",
remedio: "RX",
dipirona: 'RX',
cetaphil: 'Dermo',
sabonete: 'Dermo',
dermo: 'Dermo',
vitamina: 'Nutrição',
suplemento: 'Nutrição',
generico: 'Genérico',
diabetes: 'Diabetes',
insulina: 'Diabetes',
medicamento: 'RX',
remedio: 'RX',
};
export async function POST(request: NextRequest) {
try {
console.log("🔗 [API] Iniciando relacionamento de categorias...");
const databases = createDatabasesClient();
// 1. Buscar todas as categorias
const categoriasResponse = await databases.listDocuments(
DATABASE_ID,
COLLECTION_CATEGORIAS,
[Query.limit(100)]
);
console.log(
`📋 [API] Encontradas ${categoriasResponse.documents.length} categorias`
);
const categorias = categoriasResponse.documents;
const categoriasPorNome: { [key: string]: string } = {};
categorias.forEach((cat) => {
categoriasPorNome[cat.nome.toLowerCase()] = cat.$id;
categorias.forEach((cat: any) => {
categoriasPorNome[String(cat.nome).toLowerCase()] = cat.$id;
});
console.log(
"📋 [API] Categorias disponíveis:",
Object.keys(categoriasPorNome)
);
// 2. Buscar todos os produtos do catálogo
const produtosResponse = await databases.listDocuments(
DATABASE_ID,
COLLECTION_CATALOGO,
[Query.limit(100)]
);
console.log(
`📦 [API] Encontrados ${produtosResponse.documents.length} produtos`
);
const produtos = produtosResponse.documents;
const resultados = [];
// 3. Para cada produto, determinar e atribuir categoria
for (const produto of produtos) {
const descricao = produto.descricao.toLowerCase();
for (const produto of produtos as any[]) {
const descricao = String(produto.descricao || '').toLowerCase();
let categoriaEncontrada = null;
let nomeCategoria = "";
let nomeCategoria = '';
// Tentar encontrar categoria por palavra-chave
for (const [palavra, nomeCategoriaMapeada] of Object.entries(
mapeamentoCategorias
)) {
for (const [palavra, nomeCategoriaMapeada] of Object.entries(mapeamentoCategorias)) {
if (descricao.includes(palavra)) {
const categoriaId =
categoriasPorNome[nomeCategoriaMapeada.toLowerCase()];
const categoriaId = categoriasPorNome[nomeCategoriaMapeada.toLowerCase()];
if (categoriaId) {
categoriaEncontrada = categoriaId;
nomeCategoria = nomeCategoriaMapeada;
@ -88,26 +77,17 @@ export async function POST(request: NextRequest) {
}
}
// Se não encontrou, usar RX como padrão
if (!categoriaEncontrada) {
categoriaEncontrada =
categoriasPorNome["rx"] || categoriasPorNome["medicamento"];
nomeCategoria = "RX";
categoriaEncontrada = categoriasPorNome['rx'] || categoriasPorNome['medicamento'];
nomeCategoria = 'RX';
}
console.log(
`📦 [API] Produto: ${produto.descricao} → Categoria: ${nomeCategoria}`
);
try {
// Atualizar produto com categoria
await databases.updateDocument(
DATABASE_ID,
COLLECTION_CATALOGO,
produto.$id,
{
categorias: categoriaEncontrada,
}
{ categorias: categoriaEncontrada }
);
resultados.push({
@ -116,63 +96,47 @@ export async function POST(request: NextRequest) {
categoria: nomeCategoria,
sucesso: true,
});
console.log(
`✅ [API] Produto ${produto.$id} atualizado com categoria ${nomeCategoria}`
);
} catch (updateError) {
console.error(
`❌ [API] Erro ao atualizar produto ${produto.$id}:`,
updateError
);
resultados.push({
produtoId: produto.$id,
descricao: produto.descricao,
categoria: nomeCategoria,
sucesso: false,
erro:
updateError instanceof Error
? updateError.message
: String(updateError),
erro: updateError instanceof Error ? updateError.message : String(updateError),
});
}
// Pequena pausa para não sobrecarregar
await new Promise((resolve) => setTimeout(resolve, 100));
}
console.log("✅ [API] Processo de relacionamento concluído!");
return NextResponse.json({
sucesso: true,
mensagem: "Relacionamento de categorias concluído",
mensagem: 'Relacionamento de categorias concluído',
totalProdutos: produtos.length,
resultados: resultados,
resultados,
categoriasDisponiveis: Object.keys(categoriasPorNome),
});
} catch (error) {
console.error("❌ [API] Erro no relacionamento:", error);
console.error('[API] Erro no relacionamento:', error);
return NextResponse.json(
{
sucesso: false,
erro: error instanceof Error ? error.message : String(error),
mensagem: "Erro ao relacionar categorias",
mensagem: 'Erro ao relacionar categorias',
},
{ status: 500 }
);
}
}
export async function GET(request: NextRequest) {
export async function GET(_request: NextRequest) {
return NextResponse.json({
mensagem: "Use POST para executar o relacionamento de categorias",
mensagem: 'Use POST para executar o relacionamento de categorias',
instrucoes: [
"1. Faça uma requisição POST para esta rota",
"2. O sistema irá relacionar automaticamente produtos com categorias",
"3. Após a execução, vá em /produtos para verificar",
'1. Faça uma requisição POST para esta rota',
'2. O sistema irá relacionar automaticamente produtos com categorias',
'3. Após a execução, vá em /produtos para verificar',
],
});
}

View file

@ -1,35 +1,36 @@
import { NextRequest, NextResponse } from "next/server";
import { databases } from "@/lib/appwrite";
import { NextRequest, NextResponse } from 'next/server';
import { Client, Databases } from 'node-appwrite';
// Runtime edge para melhor performance
export const runtime = "edge";
export const runtime = 'edge';
const DATABASE_ID = process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID!;
const COLLECTION_CATALOGO =
process.env.NEXT_PUBLIC_APPWRITE_COLLECTION_CATALOGO_PRODUTOS_ID!;
const COLLECTION_CATALOGO = process.env.NEXT_PUBLIC_APPWRITE_COLLECTION_CATALOGO_PRODUTOS_ID!;
const createDatabasesClient = () => {
const apiKey = process.env.APPWRITE_API_KEY;
const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT;
const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID;
if (!apiKey || !endpoint || !projectId) {
throw new Error('Configuração do Appwrite ausente');
}
const client = new Client().setEndpoint(endpoint).setProject(projectId).setKey(apiKey);
return new Databases(client);
};
export async function POST(request: NextRequest) {
try {
const { produtoId, categoriaId, subcategoriaId } = await request.json();
console.log("🔗 [API] Relacionando produto com categoria:", {
produtoId,
categoriaId,
subcategoriaId,
});
if (!produtoId?.trim()) {
return NextResponse.json(
{
sucesso: false,
erro: "ID do produto é obrigatório",
},
{ sucesso: false, erro: 'ID do produto é obrigatório' },
{ status: 400 }
);
}
// Preparar dados para atualização
const updateData: any = {};
const updateData: Record<string, string> = {};
if (categoriaId?.trim()) {
updateData.categoria = categoriaId.trim();
@ -41,15 +42,12 @@ export async function POST(request: NextRequest) {
if (Object.keys(updateData).length === 0) {
return NextResponse.json(
{
sucesso: false,
erro: "Pelo menos uma categoria ou subcategoria deve ser fornecida",
},
{ sucesso: false, erro: 'Pelo menos uma categoria ou subcategoria deve ser fornecida' },
{ status: 400 }
);
}
// Atualizar produto no catálogo
const databases = createDatabasesClient();
const produtoAtualizado = await databases.updateDocument(
DATABASE_ID,
COLLECTION_CATALOGO,
@ -57,24 +55,19 @@ export async function POST(request: NextRequest) {
updateData
);
console.log(
"✅ [API] Produto relacionado com sucesso:",
produtoAtualizado.$id
);
return NextResponse.json({
sucesso: true,
produto: produtoAtualizado,
mensagem: "Produto relacionado com categoria/subcategoria com sucesso!",
mensagem: 'Produto relacionado com categoria/subcategoria com sucesso!',
});
} catch (error) {
console.error("❌ [API] Erro ao relacionar produto:", error);
console.error('[API] Erro ao relacionar produto:', error);
return NextResponse.json(
{
sucesso: false,
erro: error instanceof Error ? error.message : "Erro desconhecido",
mensagem: "Erro ao relacionar produto com categoria",
erro: error instanceof Error ? error.message : 'Erro desconhecido',
mensagem: 'Erro ao relacionar produto com categoria',
},
{ status: 500 }
);

View file

@ -1,11 +1,11 @@
// @ts-nocheck
// @ts-nocheck
import React, { useState, useEffect } from 'react';
import { Models } from '@/lib/appwrite';
import { ShoppingCartIcon, MinusIcon } from '@heroicons/react/24/outline';
import { useCatalogoProdutos } from '@/hooks/useCatalogoProdutos';
import { CatalogoProdutoDocument } from '@/types/legacyEntities';
interface ItemCarrinho {
produto: Models.Document;
produto: CatalogoProdutoDocument;
quantidade: number;
}
@ -14,16 +14,16 @@ interface CatalogoComCarrinhoProps {
}
const CatalogoComCarrinho: React.FC<CatalogoComCarrinhoProps> = ({ onAdicionarAoCarrinho }) => {
const { catalogoProdutos, loading, listarCatalogo, totalCatalogoProdutos, currentPage } = useCatalogoProdutos();
const { catalogoProdutos, loading, listarCatalogo, totalCatalogoProdutos } = useCatalogoProdutos();
const [carrinho, setCarrinho] = useState<ItemCarrinho[]>([]);
const [termoBusca, setTermoBusca] = useState('');
const [quantidades, setQuantidades] = useState<Record<string, number>>({});
useEffect(() => {
listarCatalogo(1, termoBusca);
}, [termoBusca]);
}, [termoBusca, listarCatalogo]);
const adicionarAoCarrinho = (produto: Models.Document) => {
const adicionarAoCarrinho = (produto: CatalogoProdutoDocument) => {
const quantidade = quantidades[produto.$id] || 1;
const itemExistente = carrinho.find(item => item.produto.$id === produto.$id);
@ -39,7 +39,6 @@ const CatalogoComCarrinho: React.FC<CatalogoComCarrinhoProps> = ({ onAdicionarAo
setCarrinho([...carrinho, novoItem]);
}
// Reset quantidade para 1 após adicionar
setQuantidades(prev => ({ ...prev, [produto.$id]: 1 }));
if (onAdicionarAoCarrinho) {
@ -47,8 +46,6 @@ const CatalogoComCarrinho: React.FC<CatalogoComCarrinhoProps> = ({ onAdicionarAo
}
};
const removerDoCarrinho = (produtoId: string) => {
setCarrinho(carrinho.filter(item => item.produto.$id !== produtoId));
};
@ -60,12 +57,10 @@ const CatalogoComCarrinho: React.FC<CatalogoComCarrinhoProps> = ({ onAdicionarAo
}, 0);
};
const formatarPreco = (preco: number) => {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL'
}).format(preco);
};
const formatarPreco = (preco: number) => new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL'
}).format(preco);
if (loading) {
return (
@ -77,7 +72,6 @@ const CatalogoComCarrinho: React.FC<CatalogoComCarrinhoProps> = ({ onAdicionarAo
return (
<div className="space-y-6">
{/* Barra de Busca */}
<div className="bg-white p-4 rounded-lg shadow">
<div className="flex gap-4">
<input
@ -94,11 +88,10 @@ const CatalogoComCarrinho: React.FC<CatalogoComCarrinhoProps> = ({ onAdicionarAo
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Lista de Produtos */}
<div className="lg:col-span-2">
<div className="bg-white rounded-lg shadow">
<div className="p-4 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-900">Catálogo de Produtos</h2>
<h2 className="text-lg font-semibold text-gray-900">Catálogo de Produtos</h2>
<p className="text-sm text-gray-500">{totalCatalogoProdutos} produtos encontrados</p>
</div>
@ -111,14 +104,10 @@ const CatalogoComCarrinho: React.FC<CatalogoComCarrinhoProps> = ({ onAdicionarAo
<div key={produto.$id} className="p-4 hover:bg-gray-50 transition-colors">
<div className="flex items-center justify-between">
<div className="flex-1">
<h3 className="font-medium text-gray-900 mb-1">
{produto.descricao}
</h3>
<h3 className="font-medium text-gray-900 mb-1">{produto.descricao}</h3>
<div className="flex items-center gap-4 text-sm text-gray-500">
<span>³digo: {produto['codigo-interno']}</span>
{produto['codigo-ean'] && (
<span>EAN: {produto['codigo-ean']}</span>
)}
<span>Código: {produto['codigo-interno']}</span>
{produto['codigo-ean'] && <span>EAN: {produto['codigo-ean']}</span>}
{produto.laboratorios?.length > 0 ? (
<span>Lab: {produto.laboratorios[0].nome || produto.laboratorios[0]}</span>
) : produto.laboratorio && (
@ -131,29 +120,21 @@ const CatalogoComCarrinho: React.FC<CatalogoComCarrinhoProps> = ({ onAdicionarAo
)}
</div>
<div className="mt-2">
<span className="text-lg font-bold text-green-600">
{formatarPreco(preco)}
</span>
<span className="text-lg font-bold text-green-600">{formatarPreco(preco)}</span>
{produto['preco-fabrica'] && produto.pmc && produto['preco-fabrica'] !== produto.pmc && (
<span className="ml-2 text-sm text-gray-500 line-through">
{formatarPreco(produto['preco-fabrica'])}
</span>
<span className="ml-2 text-sm text-gray-500 line-through">{formatarPreco(produto['preco-fabrica'])}</span>
)}
{produto['desconto-comercial'] && produto['desconto-comercial'] > 0 && (
<div className="text-xs text-orange-600 mt-1">
Desconto: {formatarPreco(produto['desconto-comercial'])}
</div>
<div className="text-xs text-orange-600 mt-1">Desconto: {formatarPreco(produto['desconto-comercial'])}</div>
)}
</div>
</div>
<div className="flex items-center gap-3">
{/* Quantidade */}
<div className="flex items-center border border-gray-300 rounded-lg px-3 py-1">
<span className="text-sm font-medium">Qtd: {quantidade}</span>
</div>
{/* Botão Adicionar ao Carrinho */}
<button
onClick={() => adicionarAoCarrinho(produto)}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
@ -170,7 +151,6 @@ const CatalogoComCarrinho: React.FC<CatalogoComCarrinhoProps> = ({ onAdicionarAo
</div>
</div>
{/* Carrinho */}
<div className="lg:col-span-1">
<div className="bg-white rounded-lg shadow sticky top-4">
<div className="p-4 border-b border-gray-200">
@ -190,17 +170,11 @@ const CatalogoComCarrinho: React.FC<CatalogoComCarrinhoProps> = ({ onAdicionarAo
return (
<div key={item.produto.$id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex-1">
<h4 className="font-medium text-sm text-gray-900 line-clamp-2">
{item.produto.descricao}
</h4>
<p className="text-xs text-gray-500">
{formatarPreco(preco)} x {item.quantidade}
</p>
<h4 className="font-medium text-sm text-gray-900 line-clamp-2">{item.produto.descricao}</h4>
<p className="text-xs text-gray-500">{formatarPreco(preco)} x {item.quantidade}</p>
</div>
<div className="flex items-center gap-2">
<span className="font-medium text-green-600">
{formatarPreco(preco * item.quantidade)}
</span>
<span className="font-medium text-green-600">{formatarPreco(preco * item.quantidade)}</span>
<button
onClick={() => removerDoCarrinho(item.produto.$id)}
className="text-red-500 hover:text-red-700 transition-colors"
@ -215,9 +189,7 @@ const CatalogoComCarrinho: React.FC<CatalogoComCarrinhoProps> = ({ onAdicionarAo
<div className="border-t border-gray-200 pt-3 mt-4">
<div className="flex justify-between items-center mb-4">
<span className="font-semibold text-gray-900">Total:</span>
<span className="font-bold text-lg text-green-600">
{formatarPreco(calcularTotal())}
</span>
<span className="font-bold text-lg text-green-600">{formatarPreco(calcularTotal())}</span>
</div>
<button className="w-full bg-green-600 text-white py-3 rounded-lg hover:bg-green-700 transition-colors font-medium">
@ -235,4 +207,3 @@ const CatalogoComCarrinho: React.FC<CatalogoComCarrinhoProps> = ({ onAdicionarAo
};
export default CatalogoComCarrinho;

View file

@ -1,12 +1,12 @@
// @ts-nocheck
// @ts-nocheck
'use client';
import React, { useState } from 'react';
import { Models } from '@/lib/appwrite';
import { useCarrinho } from '@/contexts/CarrinhoContext';
import { ShoppingCartIcon, PlusIcon, CheckIcon } from '@heroicons/react/24/outline';
import { ShoppingCartIcon, CheckIcon } from '@heroicons/react/24/outline';
import { CatalogoProdutoDocument } from '@/types/legacyEntities';
interface CatalogoProdutosComprasProps {
produtos: Models.Document[];
produtos: CatalogoProdutoDocument[];
loading: boolean;
isChangingPage?: boolean;
totalProdutos: number;
@ -14,7 +14,7 @@ interface CatalogoProdutosComprasProps {
pageSize: number;
onPrevPage: () => void;
onNextPage: () => void;
onRowClick?: (produto: Models.Document) => void;
onRowClick?: (produto: CatalogoProdutoDocument) => void;
}
const CatalogoProdutosCompras: React.FC<CatalogoProdutosComprasProps> = ({
@ -31,27 +31,23 @@ const CatalogoProdutosCompras: React.FC<CatalogoProdutosComprasProps> = ({
const { adicionarItem } = useCarrinho();
const [produtosAdicionados, setProdutosAdicionados] = useState<Set<string>>(new Set());
const formatarPreco = (preco: number) => {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL'
}).format(preco);
};
const formatarPreco = (preco: number) => new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL'
}).format(preco);
const obterPreco = (produto: Models.Document) => {
const obterPreco = (produto: CatalogoProdutoDocument) => {
return produto['preco-unitario'] || produto['preco-fabrica'] || produto.pmc || 0;
};
const obterQuantidadeDisponivel = (produto: Models.Document) => {
// Usar quantidade real do produto ou retornar 0 se não informado
return (produto as any).quantidade || (produto as any).estoque || 0;
const obterQuantidadeDisponivel = (produto: CatalogoProdutoDocument) => {
return produto.quantidade || produto.estoque || 0;
};
const handleAdicionarAoCarrinho = (produto: Models.Document, event: React.MouseEvent) => {
event.stopPropagation(); // Evita que o clique no botão dispare o onRowClick
adicionarItem(produto, 1);
const handleAdicionarAoCarrinho = (produto: CatalogoProdutoDocument, event: React.MouseEvent) => {
event.stopPropagation();
adicionarItem(produto as any, 1);
// Adicionar feedback visual temporário
setProdutosAdicionados(prev => new Set(prev).add(produto.$id));
setTimeout(() => {
setProdutosAdicionados(prev => {
@ -79,7 +75,6 @@ const CatalogoProdutosCompras: React.FC<CatalogoProdutosComprasProps> = ({
return (
<div className="bg-white rounded-lg shadow-sm">
{/* Grid de Produtos */}
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{produtos.map(produto => {
@ -100,7 +95,6 @@ const CatalogoProdutosCompras: React.FC<CatalogoProdutosComprasProps> = ({
}`}
onClick={() => onRowClick && onRowClick(produto)}
>
{/* Indicador de Estoque Baixo */}
{estoqueStatus === 'baixo' && (
<div className="absolute top-2 right-2">
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
@ -111,36 +105,25 @@ const CatalogoProdutosCompras: React.FC<CatalogoProdutosComprasProps> = ({
{estoqueStatus === 'medio' && (
<div className="absolute top-2 right-2">
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
Estoque Médio
Estoque Médio
</span>
</div>
)}
{/* Header do Card */}
<div className="flex items-start justify-between mb-3">
<div className={`flex-1 ${estoqueStatus !== 'alto' ? 'pr-20' : ''}`}>
<h3 className="text-sm font-semibold text-gray-900 line-clamp-2 mb-1">
{produto.descricao}
</h3>
<p className="text-xs text-gray-500">
³digo: {produto['codigo-interno']}
</p>
<h3 className="text-sm font-semibold text-gray-900 line-clamp-2 mb-1">{produto.descricao}</h3>
<p className="text-xs text-gray-500">Código: {produto['codigo-interno']}</p>
</div>
</div>
{/* Informações do Produto */}
<div className="space-y-3 mb-4">
{produto['codigo-ean'] && (
<p className="text-xs text-gray-500">
EAN: {produto['codigo-ean']}
</p>
)}
{produto['codigo-ean'] && <p className="text-xs text-gray-500">EAN: {produto['codigo-ean']}</p>}
{/* Laboratório */}
{produto.laboratorios?.length > 0 ? (
<div className="flex flex-wrap gap-1">
{produto.laboratorios.slice(0, 2).map((lab: any) => (
<span key={lab.$id} className="inline-block bg-gray-100 text-gray-700 px-2 py-1 rounded text-xs">
{lab.nome}
{produto.laboratorios.slice(0, 2).map((lab: any, index: number) => (
<span key={lab.$id || index} className="inline-block bg-gray-100 text-gray-700 px-2 py-1 rounded text-xs">
{lab.nome || lab}
</span>
))}
{produto.laboratorios.length > 2 && (
@ -148,19 +131,17 @@ const CatalogoProdutosCompras: React.FC<CatalogoProdutosComprasProps> = ({
)}
</div>
) : (
<span className="text-xs text-gray-400">Sem laboratório</span>
<span className="text-xs text-gray-400">Sem laboratório</span>
)}
{/* Categoria */}
{produto.categorias && (
<span className="inline-block bg-blue-100 text-blue-700 px-2 py-1 rounded text-xs">
{produto.categorias.nome}
</span>
)}
{/* Quantidade Disponível */}
<div className="flex items-center justify-between text-xs">
<span className="text-gray-600">Disponível:</span>
<span className="text-gray-600">Disponível:</span>
<span className={`font-medium ${
estoqueStatus === 'alto'
? 'text-green-600'
@ -173,44 +154,32 @@ const CatalogoProdutosCompras: React.FC<CatalogoProdutosComprasProps> = ({
</div>
</div>
{/* Seção de Preços */}
<div className="space-y-2 mb-4">
{/* Preço Principal */}
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Preço:</span>
<span className="text-sm text-gray-600">Preço:</span>
{preco > 0 ? (
<span className="text-lg font-bold text-green-600">
{formatarPreco(preco)}
</span>
<span className="text-lg font-bold text-green-600">{formatarPreco(preco)}</span>
) : (
<span className="text-sm text-gray-400">
£o informado
</span>
<span className="text-sm text-gray-400">Não informado</span>
)}
</div>
{/* Preços Adicionais */}
<div className="grid grid-cols-2 gap-2 text-xs">
{produto['preco-fabrica'] && produto['preco-fabrica'] !== preco && (
<div className="text-center">
<span className="block text-gray-500">¡brica</span>
<span className="font-medium text-gray-700">
{formatarPreco(produto['preco-fabrica'])}
</span>
<span className="block text-gray-500">Fábrica</span>
<span className="font-medium text-gray-700">{formatarPreco(produto['preco-fabrica'])}</span>
</div>
)}
{produto.pmc && produto.pmc !== preco && (
<div className="text-center">
<span className="block text-gray-500">PMC</span>
<span className="font-medium text-gray-700">
{formatarPreco(produto.pmc)}
</span>
<span className="font-medium text-gray-700">{formatarPreco(produto.pmc)}</span>
</div>
)}
</div>
</div>
{/* Botão de Ação */}
<div className="flex justify-center">
<button
onClick={(e) => handleAdicionarAoCarrinho(produto, e)}
@ -241,17 +210,15 @@ const CatalogoProdutosCompras: React.FC<CatalogoProdutosComprasProps> = ({
})}
</div>
{/* Mensagem quando não há produtos */}
{produtos.length === 0 && (
<div className="text-center py-12">
<ShoppingCartIcon className="w-12 h-12 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">Nenhum produto encontrado</h3>
<p className="text-gray-500">Tente ajustar os filtros de busca ou verifique se há produtos cadastrados.</p>
<p className="text-gray-500">Tente ajustar os filtros de busca ou verifique se há produtos cadastrados.</p>
</div>
)}
</div>
{/* Paginação */}
{produtos.length > 0 && (
<div className="bg-gray-50 border-t border-gray-200 px-6 py-4">
<div className="flex items-center justify-between">
@ -263,15 +230,13 @@ const CatalogoProdutosCompras: React.FC<CatalogoProdutosComprasProps> = ({
>
Anterior
</button>
<span className="px-3 py-2 text-sm text-gray-700 font-medium">
¡gina {currentPage} de {totalPages}
</span>
<span className="px-3 py-2 text-sm text-gray-700 font-medium">Página {currentPage} de {totalPages}</span>
<button
onClick={onNextPage}
disabled={currentPage >= totalPages || isChangingPage}
className="px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200"
>
Próxima
Próxima
</button>
</div>
<div className="text-sm text-gray-700 font-medium">

View file

@ -1,18 +1,8 @@
import { Models } from "@/lib/appwrite";
import { CatalogoProdutoDocument } from '@/types/legacyEntities';
export interface CatalogoProdutoData extends Models.Document {
descricao: string;
nome?: string;
"codigo-interno"?: string;
"codigo-ean"?: string;
"preco-original"?: number;
"preco-atual"?: number;
"preco-fabrica"?: number;
pmc?: number;
quantidade?: number;
laboratorio?: string;
categoria?: string;
subcategoria?: string;
export interface CatalogoProdutoData extends CatalogoProdutoDocument {
'preco-original'?: number;
'preco-atual'?: number;
}
export interface ApiResponse<T = unknown> {
@ -28,14 +18,14 @@ export interface PaginatedResponse<T> extends ApiResponse<T[]> {
}
class CatalogoProdutoService {
private readonly baseUrl = "/api/catalogo-produtos";
private readonly baseUrl = '/api/catalogo-produtos';
private async parseResponse<T>(response: Response): Promise<T> {
const data = await response.json().catch(() => ({}));
if (!response.ok) {
const message =
typeof data?.error === "string"
typeof data?.error === 'string'
? data.error
: `Erro HTTP ${response.status}`;
throw new Error(message);
@ -44,36 +34,36 @@ class CatalogoProdutoService {
return data as T;
}
async listar(page = 1, limit = 10, termo = ""): Promise<PaginatedResponse<CatalogoProdutoData>> {
async listar(page = 1, limit = 10, termo = ''): Promise<PaginatedResponse<CatalogoProdutoData>> {
const params = new URLSearchParams({
page: String(page),
limit: String(limit),
});
if (termo.trim()) {
params.set("q", termo.trim());
params.set('q', termo.trim());
}
const response = await fetch(`${this.baseUrl}?${params.toString()}`, {
method: "GET",
cache: "no-store",
method: 'GET',
cache: 'no-store',
});
const data = await this.parseResponse<Partial<PaginatedResponse<CatalogoProdutoData>>>(response);
const documents = Array.isArray(data.documents) ? data.documents : [];
const filteredDocuments = termo.trim()
? documents.filter((produto) => {
? documents.filter(produto => {
const values = [
produto.descricao,
produto.nome,
produto["codigo-interno"],
produto["codigo-ean"],
produto['codigo-interno'],
produto['codigo-ean'],
]
.filter(Boolean)
.map((value) => String(value).toLowerCase());
.map(value => String(value).toLowerCase());
return values.some((value) => value.includes(termo.trim().toLowerCase()));
return values.some(value => value.includes(termo.trim().toLowerCase()));
})
: documents;
@ -90,8 +80,8 @@ class CatalogoProdutoService {
async obterPorId(id: string): Promise<ApiResponse<CatalogoProdutoData>> {
const response = await fetch(`${this.baseUrl}/${id}`, {
method: "GET",
cache: "no-store",
method: 'GET',
cache: 'no-store',
});
const data = await this.parseResponse<ApiResponse<CatalogoProdutoData>>(response);
@ -103,9 +93,9 @@ class CatalogoProdutoService {
async criar(produtoData: Partial<CatalogoProdutoData>): Promise<ApiResponse<CatalogoProdutoData>> {
const response = await fetch(this.baseUrl, {
method: "POST",
method: 'POST',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
body: JSON.stringify(produtoData),
});
@ -119,9 +109,9 @@ class CatalogoProdutoService {
async atualizar(id: string, produtoData: Partial<CatalogoProdutoData>): Promise<ApiResponse<CatalogoProdutoData>> {
const response = await fetch(`${this.baseUrl}/${id}`, {
method: "PATCH",
method: 'PATCH',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
body: JSON.stringify(produtoData),
});
@ -135,7 +125,7 @@ class CatalogoProdutoService {
async deletar(id: string): Promise<ApiResponse<null>> {
const response = await fetch(`${this.baseUrl}/${id}`, {
method: "DELETE",
method: 'DELETE',
});
await this.parseResponse<ApiResponse<null>>(response);
@ -145,4 +135,3 @@ class CatalogoProdutoService {
export const catalogoProdutoService = new CatalogoProdutoService();
export type ProdutoData = CatalogoProdutoData;