refactor(saveinmed): type legacy addresses and parameterize bootstrap admin

This commit is contained in:
Tiago Yamamoto 2026-03-07 08:24:53 -06:00
parent 7e3604ed4a
commit 2829a3e87c
7 changed files with 219 additions and 230 deletions

View file

@ -27,6 +27,8 @@ type Config struct {
SwaggerSchemes []string SwaggerSchemes []string
MercadoPagoPublicKey string MercadoPagoPublicKey string
MapboxAccessToken string MapboxAccessToken string
BootstrapAdminEmail string
BootstrapAdminPassword string
} }
// Load reads configuration from environment variables and applies sane defaults // Load reads configuration from environment variables and applies sane defaults
@ -50,6 +52,8 @@ func Load() (*Config, error) {
SwaggerSchemes: getEnvStringSlice("SWAGGER_SCHEMES", []string{"http"}), SwaggerSchemes: getEnvStringSlice("SWAGGER_SCHEMES", []string{"http"}),
MercadoPagoPublicKey: getEnv("MERCADOPAGO_PUBLIC_KEY", "TEST-PUBLIC-KEY"), MercadoPagoPublicKey: getEnv("MERCADOPAGO_PUBLIC_KEY", "TEST-PUBLIC-KEY"),
MapboxAccessToken: getEnv("MAPBOX_ACCESS_TOKEN", ""), MapboxAccessToken: getEnv("MAPBOX_ACCESS_TOKEN", ""),
BootstrapAdminEmail: getEnv("BOOTSTRAP_ADMIN_EMAIL", "admin@saveinmed.com.br"),
BootstrapAdminPassword: getEnv("BOOTSTRAP_ADMIN_PASSWORD", "sim-admin"),
} }
return &cfg, nil return &cfg, nil

View file

@ -25,8 +25,6 @@ import (
const ( const (
bootstrapAdminName = "SaveInMed Admin" bootstrapAdminName = "SaveInMed Admin"
bootstrapAdminUsername = "admin" bootstrapAdminUsername = "admin"
bootstrapAdminEmail = "admin@saveinmed.com"
bootstrapAdminPassword = "sim-admin"
) )
// Server wires the infrastructure and exposes HTTP handlers. // Server wires the infrastructure and exposes HTTP handlers.
@ -223,15 +221,16 @@ func (s *Server) Start(ctx context.Context) error {
// Seed platform admin automatically // Seed platform admin automatically
{ {
existingUser, err := repo.GetUserByEmail(ctx, bootstrapAdminEmail) adminEmail := s.cfg.BootstrapAdminEmail
if err != nil { adminPassword := s.cfg.BootstrapAdminPassword
log.Printf("Seeding admin user: %s", bootstrapAdminEmail)
existingUser, err := repo.GetUserByEmail(ctx, adminEmail)
if err != nil {
log.Printf("Seeding admin user: %s", adminEmail)
// 1. Create/Get platform company
adminCNPJ := "00000000000000"
company := &domain.Company{ company := &domain.Company{
ID: uuid.Nil, ID: uuid.Nil,
CNPJ: adminCNPJ, CNPJ: "00000000000000",
CorporateName: "SaveInMed Platform", CorporateName: "SaveInMed Platform",
Category: "platform", Category: "platform",
LicenseNumber: "ADMIN", LicenseNumber: "ADMIN",
@ -240,46 +239,21 @@ func (s *Server) Start(ctx context.Context) error {
UpdatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(),
} }
// We need to check if company exists by CNPJ normally, but repo doesn't expose GetByCNPJ easily?
// Let's rely on RegisterAccount handling it or check if we can query.
// Actually RegisterAccount in Service handles creation if ID is Nil, but keys off ID.
// We can try to create and ignore conflict, or use a known ID?
// Let's use RegisterAccount logic.
// Because RegisterAccount expects us to pass a company, and tries to Get by ID if ID is set, or Create if not.
// But duplicate CNPJ will fail at DB level.
// Let's assume on fresh boot it doesn't exist.
// Or better: Use svc.RegisterAccount. But wait, svc.RegisterAccount logic:
/*
if company != nil {
if company.ID == uuid.Nil {
// create
} else {
// get
}
}
*/
// If we re-run, GetUserByEmail would have found the user, so we skip.
// The only edge case is if User was deleted but Company remains.
// In that case, CreateCompany will fail on CNPJ constraint.
err := s.svc.RegisterAccount(ctx, company, &domain.User{ err := s.svc.RegisterAccount(ctx, company, &domain.User{
Role: domain.RoleAdmin, Role: domain.RoleAdmin,
Name: bootstrapAdminName, Name: bootstrapAdminName,
Username: bootstrapAdminUsername, Username: bootstrapAdminUsername,
Email: bootstrapAdminEmail, Email: adminEmail,
Superadmin: false, Superadmin: false,
}, bootstrapAdminPassword) }, adminPassword)
if err != nil { if err != nil {
log.Printf("Failed to seed admin: %v", err) log.Printf("Failed to seed admin: %v", err)
} else { } else {
// FORCE VERIFY the platform company
if _, err := s.svc.VerifyCompany(ctx, company.ID); err != nil { if _, err := s.svc.VerifyCompany(ctx, company.ID); err != nil {
log.Printf("Failed to verify platform company: %v", err) log.Printf("Failed to verify platform company: %v", err)
} }
log.Printf("Admin user created successfully") log.Printf("Admin user created: email=%s", adminEmail)
log.Printf("Bootstrap admin credentials: email=%s password=%s", bootstrapAdminEmail, bootstrapAdminPassword)
} }
} else { } else {
existingUser.Role = domain.RoleAdmin existingUser.Role = domain.RoleAdmin
@ -291,11 +265,10 @@ func (s *Server) Start(ctx context.Context) error {
existingUser.Username = bootstrapAdminUsername existingUser.Username = bootstrapAdminUsername
} }
if err := s.svc.UpdateUser(ctx, existingUser, bootstrapAdminPassword); err != nil { if err := s.svc.UpdateUser(ctx, existingUser, adminPassword); err != nil {
log.Printf("Failed to reconcile existing user %s as admin: %v", bootstrapAdminEmail, err) log.Printf("Failed to reconcile existing user %s as admin: %v", adminEmail, err)
} else { } else {
log.Printf("Existing user %s reconciled as admin with bootstrap password", bootstrapAdminEmail) log.Printf("Admin user reconciled: email=%s", adminEmail)
log.Printf("Bootstrap admin credentials: email=%s password=%s", bootstrapAdminEmail, bootstrapAdminPassword)
} }
} }
} }

View file

@ -1,9 +1,8 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { getCurrentUserWithRetry, account } from '@/lib/appwrite'; import { getCurrentUserWithRetry, Models } from '@/lib/appwrite';
import { Models } from '@/lib/appwrite';
import Header from '@/components/Header'; import Header from '@/components/Header';
import { MapPin, Plus } from 'lucide-react'; import { MapPin, Plus } from 'lucide-react';
import EnderecoForm from '@/components/EnderecoForm'; import EnderecoForm from '@/components/EnderecoForm';
@ -11,13 +10,15 @@ import EnderecoList from '@/components/EnderecoList';
import { useEnderecos, EnderecoData } from '@/hooks/useEnderecos'; import { useEnderecos, EnderecoData } from '@/hooks/useEnderecos';
import { RoleGuard } from '@/components/auth/RoleGuard'; import { RoleGuard } from '@/components/auth/RoleGuard';
import { UserRole } from '@/types/auth'; import { UserRole } from '@/types/auth';
import { EnderecoDocument } from '@/types/legacyEntities';
const GestaoEnderecos = () => { const GestaoEnderecos = () => {
const router = useRouter(); const router = useRouter();
const [user, setUser] = useState<Models.User<Models.Preferences> | null>(null); const [user, setUser] = useState<Models.User<Models.Preferences> | null>(null);
const [editing, setEditing] = useState<Models.Document | null>(null); const [editing, setEditing] = useState<EnderecoDocument | null>(null);
const [activeTab, setActiveTab] = useState<'lista' | 'cadastro'>('lista'); const [activeTab, setActiveTab] = useState<'lista' | 'cadastro'>('lista');
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [checkingAuth, setCheckingAuth] = useState(true);
const { const {
enderecos, enderecos,
@ -32,21 +33,27 @@ const GestaoEnderecos = () => {
cadastrarEndereco, cadastrarEndereco,
atualizarEndereco, atualizarEndereco,
deletarEndereco, deletarEndereco,
setCurrentPage setCurrentPage,
} = useEnderecos(); } = useEnderecos();
useEffect(() => { useEffect(() => {
const initializeUser = async () => { const initializeUser = async () => {
try { try {
const currentUser = await account.get(); const currentUser = await getCurrentUserWithRetry();
setUser(currentUser); if (!currentUser) {
router.push('/');
return;
}
setUser(currentUser);
if (activeTab === 'lista') { if (activeTab === 'lista') {
await listarEnderecos(); await listarEnderecos();
} }
} catch (error) { } catch (error) {
console.error('❌ Usuário não autenticado:', error); console.error('Usuario nao autenticado:', error);
router.push('/'); router.push('/');
} finally {
setCheckingAuth(false);
} }
}; };
@ -61,16 +68,16 @@ const GestaoEnderecos = () => {
setActiveTab('lista'); setActiveTab('lista');
} }
return success; return success;
} else {
const success = await cadastrarEndereco(formData);
if (success) {
setTimeout(() => setActiveTab('lista'), 2000);
}
return success;
} }
const success = await cadastrarEndereco(formData);
if (success) {
setTimeout(() => setActiveTab('lista'), 2000);
}
return success;
}; };
const handleEdit = (endereco: Models.Document) => { const handleEdit = (endereco: EnderecoDocument) => {
setEditing(endereco); setEditing(endereco);
setActiveTab('cadastro'); setActiveTab('cadastro');
}; };
@ -90,9 +97,10 @@ const GestaoEnderecos = () => {
setSearchTerm(term); setSearchTerm(term);
if (term.trim()) { if (term.trim()) {
await buscarEnderecos(term, 1); await buscarEnderecos(term, 1);
} else { return;
await listarEnderecos(1);
} }
await listarEnderecos(1);
}; };
const handleNextPage = () => { const handleNextPage = () => {
@ -102,12 +110,12 @@ const GestaoEnderecos = () => {
} }
}; };
if (!user) { if (checkingAuth || !user) {
return ( return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center"> <div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto mb-4"></div> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto mb-4"></div>
<p className="text-gray-600">Verificando autenticação...</p> <p className="text-gray-600">Verificando autenticacao...</p>
</div> </div>
</div> </div>
); );
@ -118,72 +126,72 @@ const GestaoEnderecos = () => {
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
<Header <Header
user={user} user={user}
title="Gestão de Endereços" title="Gestao de Enderecos"
subtitle="Gerencie os endereços da plataforma SaveInMed" subtitle="Gerencie os enderecos da plataforma SaveInMed"
/> />
<main className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8"> <main className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<div className="mb-6"> <div className="mb-6">
<nav className="flex space-x-8 bg-white p-4 rounded-lg shadow-sm"> <nav className="flex space-x-8 bg-white p-4 rounded-lg shadow-sm">
<button <button
onClick={() => { onClick={() => {
setActiveTab('lista'); setActiveTab('lista');
setEditing(null); setEditing(null);
}} }}
className={`py-2 px-4 border-b-2 font-medium text-sm transition-colors cursor-pointer rounded-t-md ${ className={`py-2 px-4 border-b-2 font-medium text-sm transition-colors cursor-pointer rounded-t-md ${
activeTab === 'lista' activeTab === 'lista'
? 'border-gray-900 text-gray-900 bg-gray-100' ? 'border-gray-900 text-gray-900 bg-gray-100'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 hover:bg-gray-50' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 hover:bg-gray-50'
}`} }`}
> >
<MapPin className="inline-block w-4 h-4 mr-1" /> Listar Endereços <MapPin className="inline-block w-4 h-4 mr-1" /> Listar Enderecos
</button> </button>
<button <button
onClick={() => { onClick={() => {
setActiveTab('cadastro'); setActiveTab('cadastro');
setEditing(null); setEditing(null);
}} }}
className={`py-2 px-4 border-b-2 font-medium text-sm transition-colors cursor-pointer rounded-t-md ${ className={`py-2 px-4 border-b-2 font-medium text-sm transition-colors cursor-pointer rounded-t-md ${
activeTab === 'cadastro' activeTab === 'cadastro'
? 'border-indigo-500 text-indigo-600 bg-indigo-50' ? 'border-indigo-500 text-indigo-600 bg-indigo-50'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 hover:bg-gray-50' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 hover:bg-gray-50'
}`} }`}
> >
<Plus className="inline-block w-4 h-4 mr-1" /> Cadastrar Endereço <Plus className="inline-block w-4 h-4 mr-1" /> Cadastrar Endereco
</button> </button>
</nav> </nav>
</div> </div>
{activeTab === 'lista' && ( {activeTab === 'lista' && (
<EnderecoList <EnderecoList
enderecos={enderecos} enderecos={enderecos}
loading={loading} loading={loading}
isChangingPage={isChangingPage} isChangingPage={isChangingPage}
error={error} error={error}
totalEnderecos={totalEnderecos} totalEnderecos={totalEnderecos}
currentPage={currentPage} currentPage={currentPage}
pageSize={10} pageSize={10}
onEdit={handleEdit} onEdit={handleEdit}
onDelete={deletarEndereco} onDelete={deletarEndereco}
onRefresh={() => onRefresh={() =>
searchTerm.trim() searchTerm.trim()
? buscarEnderecos(searchTerm, currentPage) ? buscarEnderecos(searchTerm, currentPage)
: listarEnderecos(currentPage) : listarEnderecos(currentPage)
} }
onPrevPage={handlePrevPage} onPrevPage={handlePrevPage}
onNextPage={handleNextPage} onNextPage={handleNextPage}
onSearch={handleSearch} onSearch={handleSearch}
/> />
)} )}
{activeTab === 'cadastro' && ( {activeTab === 'cadastro' && (
<EnderecoForm <EnderecoForm
onSubmit={handleFormSubmit} onSubmit={handleFormSubmit}
onCancel={editing ? handleCancelEdit : undefined} onCancel={editing ? handleCancelEdit : undefined}
initialData={editing} initialData={editing}
loading={isCreating || loading} loading={isCreating || loading}
/> />
)} )}
</main> </main>
</div> </div>
</RoleGuard> </RoleGuard>
@ -191,4 +199,3 @@ const GestaoEnderecos = () => {
}; };
export default GestaoEnderecos; export default GestaoEnderecos;

View file

@ -1,16 +1,14 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Models } from '@/lib/appwrite';
import { EnderecoData } from '@/services/enderecoService'; import { EnderecoData } from '@/services/enderecoService';
import { EnderecoDocument } from '@/types/legacyEntities';
interface EnderecoFormProps { interface EnderecoFormProps {
onSubmit: (data: EnderecoData) => Promise<boolean>; onSubmit: (data: EnderecoData) => Promise<boolean>;
onCancel?: () => void; onCancel?: () => void;
initialData?: Models.Document | null; initialData?: EnderecoDocument | null;
loading?: boolean; loading?: boolean;
} }
type EnderecoDocument = Models.Document & EnderecoData;
const buildEmptyEndereco = (): EnderecoData => ({ const buildEmptyEndereco = (): EnderecoData => ({
titulo: '', titulo: '',
cep: '', cep: '',
@ -42,19 +40,18 @@ const EnderecoForm: React.FC<EnderecoFormProps> = ({
return; return;
} }
const document = initialData as EnderecoDocument;
setFormData({ setFormData({
titulo: document.titulo || '', titulo: initialData.titulo || '',
cep: document.cep || '', cep: initialData.cep || '',
logradouro: document.logradouro || '', logradouro: initialData.logradouro || '',
bairro: document.bairro || '', bairro: initialData.bairro || '',
numero: document.numero || '', numero: initialData.numero || '',
complemento: document.complemento || '', complemento: initialData.complemento || '',
cidade: document.cidade || '', cidade: initialData.cidade || '',
estado: document.estado || '', estado: initialData.estado || '',
pais: document.pais || '', pais: initialData.pais || '',
latitude: Number(document.latitude) || 0, latitude: Number(initialData.latitude) || 0,
longitude: Number(document.longitude) || 0, longitude: Number(initialData.longitude) || 0,
}); });
}, [initialData]); }, [initialData]);
@ -68,7 +65,7 @@ const EnderecoForm: React.FC<EnderecoFormProps> = ({
try { try {
const res = await fetch(`https://lernu-backend-dev.rede5.com.br/viacep/${digits}`); const res = await fetch(`https://lernu-backend-dev.rede5.com.br/viacep/${digits}`);
if (!res.ok) { if (!res.ok) {
throw new Error('CEP inválido'); throw new Error('CEP invalido');
} }
const data = (await res.json()) as Partial<EnderecoData>; const data = (await res.json()) as Partial<EnderecoData>;
@ -83,7 +80,7 @@ const EnderecoForm: React.FC<EnderecoFormProps> = ({
setCepError(null); setCepError(null);
} catch (error) { } catch (error) {
console.error('Erro ao buscar CEP:', error); console.error('Erro ao buscar CEP:', error);
setCepError('CEP não encontrado.'); setCepError('CEP nao encontrado.');
} }
}; };
@ -98,7 +95,7 @@ const EnderecoForm: React.FC<EnderecoFormProps> = ({
if (success) { if (success) {
setMessage({ setMessage({
type: 'success', type: 'success',
text: initialData ? 'Endereço atualizado com sucesso!' : 'Endereço cadastrado com sucesso!', text: initialData ? 'Endereco atualizado com sucesso!' : 'Endereco cadastrado com sucesso!',
}); });
if (!initialData) { if (!initialData) {
@ -111,7 +108,7 @@ const EnderecoForm: React.FC<EnderecoFormProps> = ({
setMessage({ setMessage({
type: 'error', type: 'error',
text: initialData ? 'Erro ao atualizar endereço' : 'Erro ao cadastrar endereço', text: initialData ? 'Erro ao atualizar endereco' : 'Erro ao cadastrar endereco',
}); });
}; };
@ -149,10 +146,10 @@ const EnderecoForm: React.FC<EnderecoFormProps> = ({
<div className="bg-white rounded-lg shadow-md p-6"> <div className="bg-white rounded-lg shadow-md p-6">
<div className="mb-6"> <div className="mb-6">
<h2 className="text-2xl font-bold text-gray-900 mb-2"> <h2 className="text-2xl font-bold text-gray-900 mb-2">
{initialData ? 'Editar Endereço' : 'Novo Endereço'} {initialData ? 'Editar Endereco' : 'Novo Endereco'}
</h2> </h2>
<p className="text-gray-600"> <p className="text-gray-600">
{initialData ? 'Atualize os dados do endereço.' : 'Cadastre um novo endereço na plataforma SaveInMed.'} {initialData ? 'Atualize os dados do endereco.' : 'Cadastre um novo endereco na plataforma SaveInMed.'}
</p> </p>
</div> </div>
@ -170,7 +167,7 @@ const EnderecoForm: React.FC<EnderecoFormProps> = ({
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label htmlFor="titulo" className="block text-sm font-medium text-gray-700 mb-2"> <label htmlFor="titulo" className="block text-sm font-medium text-gray-700 mb-2">
Título * Titulo *
</label> </label>
<input <input
type="text" type="text"
@ -237,7 +234,7 @@ const EnderecoForm: React.FC<EnderecoFormProps> = ({
</div> </div>
<div> <div>
<label htmlFor="numero" className="block text-sm font-medium text-gray-700 mb-2"> <label htmlFor="numero" className="block text-sm font-medium text-gray-700 mb-2">
Número * Numero *
</label> </label>
<input <input
type="text" type="text"
@ -301,7 +298,7 @@ const EnderecoForm: React.FC<EnderecoFormProps> = ({
</div> </div>
<div> <div>
<label htmlFor="pais" className="block text-sm font-medium text-gray-700 mb-2"> <label htmlFor="pais" className="block text-sm font-medium text-gray-700 mb-2">
País * Pais *
</label> </label>
<input <input
type="text" type="text"

View file

@ -1,21 +1,21 @@
import React from 'react'; import React from 'react';
import { Models } from '@/lib/appwrite';
import SearchBar from './SearchBar'; import SearchBar from './SearchBar';
import RefreshButton from './RefreshButton'; import RefreshButton from './RefreshButton';
import ListHeader from './ListHeader'; import ListHeader from './ListHeader';
import DataTable, { Column } from './DataTable'; import DataTable, { Column } from './DataTable';
import Pagination from './Pagination'; import Pagination from './Pagination';
import TableActions from './TableActions'; import TableActions from './TableActions';
import { EnderecoDocument } from '@/types/legacyEntities';
interface EnderecoListProps { interface EnderecoListProps {
enderecos: Models.Document[]; enderecos: EnderecoDocument[];
loading: boolean; loading: boolean;
isChangingPage?: boolean; isChangingPage?: boolean;
error: string | null; error: string | null;
totalEnderecos: number; totalEnderecos: number;
currentPage: number; currentPage: number;
pageSize: number; pageSize: number;
onEdit: (endereco: Models.Document) => void; onEdit: (endereco: EnderecoDocument) => void;
onDelete: (id: string) => Promise<boolean>; onDelete: (id: string) => Promise<boolean>;
onRefresh: () => void; onRefresh: () => void;
onPrevPage: () => void; onPrevPage: () => void;
@ -23,6 +23,14 @@ interface EnderecoListProps {
onSearch: (termo: string) => void; onSearch: (termo: string) => void;
} }
const columns: Column<EnderecoDocument>[] = [
{ key: 'titulo', header: 'Titulo' },
{ key: 'cep', header: 'CEP' },
{ key: 'cidade', header: 'Cidade' },
{ key: 'estado', header: 'Estado' },
{ key: 'pais', header: 'Pais' },
];
const EnderecoList: React.FC<EnderecoListProps> = ({ const EnderecoList: React.FC<EnderecoListProps> = ({
enderecos, enderecos,
loading, loading,
@ -36,7 +44,7 @@ const EnderecoList: React.FC<EnderecoListProps> = ({
onRefresh, onRefresh,
onPrevPage, onPrevPage,
onNextPage, onNextPage,
onSearch onSearch,
}) => { }) => {
const totalPages = Math.ceil(totalEnderecos / pageSize); const totalPages = Math.ceil(totalEnderecos / pageSize);
const startItem = (currentPage - 1) * pageSize + 1; const startItem = (currentPage - 1) * pageSize + 1;
@ -49,27 +57,19 @@ const EnderecoList: React.FC<EnderecoListProps> = ({
}; };
const handleDelete = async (id: string) => { const handleDelete = async (id: string) => {
if (confirm('Tem certeza que deseja deletar este endereço?')) { if (confirm('Tem certeza que deseja deletar este endereco?')) {
await onDelete(id); await onDelete(id);
} }
}; };
const columns: Column<Models.Document>[] = [
{ key: 'titulo', header: 'Título' },
{ key: 'cep', header: 'CEP' },
{ key: 'cidade', header: 'Cidade' },
{ key: 'estado', header: 'Estado' },
{ key: 'pais', header: 'País' },
];
return ( return (
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<ListHeader title="Lista de Endereços"> <ListHeader title="Lista de Enderecos">
<SearchBar <SearchBar
value={search} value={search}
onChange={setSearch} onChange={setSearch}
onSearch={handleSearch} onSearch={handleSearch}
placeholder="Buscar título, CEP ou cidade" placeholder="Buscar titulo, CEP ou cidade"
/> />
<RefreshButton onClick={onRefresh} loading={loading} /> <RefreshButton onClick={onRefresh} loading={loading} />
</ListHeader> </ListHeader>
@ -84,24 +84,24 @@ const EnderecoList: React.FC<EnderecoListProps> = ({
<div className="bg-blue-50 border border-blue-200 rounded-md p-3 mb-4"> <div className="bg-blue-50 border border-blue-200 rounded-md p-3 mb-4">
<div className="flex items-center"> <div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 mr-2"></div> <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 mr-2"></div>
<span className="text-blue-800 text-sm">Carregando página...</span> <span className="text-blue-800 text-sm">Carregando pagina...</span>
</div> </div>
</div> </div>
)} )}
{loading ? ( {loading ? (
<div className="flex justify-center items-center min-h-screen"> <div className="flex justify-center items-center min-h-screen">
<div className="text-lg">Carregando endereços...</div> <div className="text-lg">Carregando enderecos...</div>
</div> </div>
) : ( ) : (
<> <>
<DataTable <DataTable
columns={columns} columns={columns}
data={enderecos} data={enderecos}
actions={(end) => ( actions={(endereco) => (
<TableActions <TableActions
onEdit={() => onEdit(end)} onEdit={() => onEdit(endereco)}
onDelete={() => handleDelete(end.$id)} onDelete={() => handleDelete(endereco.$id)}
/> />
)} )}
/> />
@ -109,9 +109,9 @@ const EnderecoList: React.FC<EnderecoListProps> = ({
<div className="mt-4 flex justify-between items-center"> <div className="mt-4 flex justify-between items-center">
<div className="text-sm text-gray-600"> <div className="text-sm text-gray-600">
{totalEnderecos > 0 ? ( {totalEnderecos > 0 ? (
<>Mostrando {startItem} - {endItem} de {totalEnderecos} endereços</> <>Mostrando {startItem} - {endItem} de {totalEnderecos} enderecos</>
) : ( ) : (
'Total de endereços: 0' 'Total de enderecos: 0'
)} )}
</div> </div>

View file

@ -1,11 +1,10 @@
// @ts-nocheck
import { useState, useCallback, useRef, useEffect } from 'react'; import { useState, useCallback, useRef, useEffect } from 'react';
import { Models } from '@/lib/appwrite';
import { enderecoService, EnderecoData } from '@/services/enderecoService'; import { enderecoService, EnderecoData } from '@/services/enderecoService';
import { EnderecoDocument } from '@/types/legacyEntities';
export type { EnderecoData }; export type { EnderecoData };
export interface UseEnderecosReturn { export interface UseEnderecosReturn {
enderecos: Models.Document[]; enderecos: EnderecoDocument[];
loading: boolean; loading: boolean;
isChangingPage: boolean; isChangingPage: boolean;
isCreating: boolean; isCreating: boolean;
@ -25,7 +24,7 @@ export interface UseEnderecosReturn {
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
export const useEnderecos = (): UseEnderecosReturn => { export const useEnderecos = (): UseEnderecosReturn => {
const [enderecos, setEnderecos] = useState<Models.Document[]>([]); const [enderecos, setEnderecos] = useState<EnderecoDocument[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [totalEnderecos, setTotalEnderecos] = useState(0); const [totalEnderecos, setTotalEnderecos] = useState(0);
@ -35,8 +34,7 @@ export const useEnderecos = (): UseEnderecosReturn => {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const isInitialMount = useRef(true); const isInitialMount = useRef(true);
const listarEnderecos = useCallback( const listarEnderecos = useCallback(async (page = currentPage, search = searchTerm) => {
async (page = currentPage, search = searchTerm) => {
setIsChangingPage(true); setIsChangingPage(true);
setLoading(true); setLoading(true);
setError(null); setError(null);
@ -47,51 +45,46 @@ export const useEnderecos = (): UseEnderecosReturn => {
: await enderecoService.listar(page, PAGE_SIZE); : await enderecoService.listar(page, PAGE_SIZE);
if (response.success) { if (response.success) {
setEnderecos(response.documents || []); setEnderecos((response.documents || []) as EnderecoDocument[]);
setTotalEnderecos(response.total || 0); setTotalEnderecos(response.total || 0);
setCurrentPage(page); setCurrentPage(page);
setSearchTerm(search); setSearchTerm(search);
} else { return;
setError(response.error || 'Erro ao carregar endereços');
} }
} catch (err) {
setError('Erro de conexão ao carregar endereços'); setError(response.error || 'Erro ao carregar enderecos');
} catch {
setError('Erro de conexao ao carregar enderecos');
} finally { } finally {
setLoading(false); setLoading(false);
setIsChangingPage(false); setIsChangingPage(false);
} }
}, [currentPage, searchTerm]); }, [currentPage, searchTerm]);
const buscarEnderecos = useCallback( const buscarEnderecos = useCallback(async (termo: string, page = 1) => {
async (termo: string, page = 1) => { setIsChangingPage(true);
setIsChangingPage(true); setLoading(true);
setLoading(true); setError(null);
setError(null);
try { try {
const response = await enderecoService.buscar( const response = await enderecoService.buscar(termo, page, PAGE_SIZE);
termo,
page,
PAGE_SIZE
);
if (response.success) { if (response.success) {
setEnderecos(response.documents || []); setEnderecos((response.documents || []) as EnderecoDocument[]);
setTotalEnderecos(response.total || 0); setTotalEnderecos(response.total || 0);
setCurrentPage(page); setCurrentPage(page);
setSearchTerm(termo); setSearchTerm(termo);
} else { return;
setError(response.error || 'Erro ao buscar enderecos');
}
} catch (err) {
setError('Erro de conexão ao buscar enderecos');
} finally {
setLoading(false);
setIsChangingPage(false);
} }
},
[] setError(response.error || 'Erro ao buscar enderecos');
); } catch {
setError('Erro de conexao ao buscar enderecos');
} finally {
setLoading(false);
setIsChangingPage(false);
}
}, []);
const cadastrarEndereco = useCallback(async (formData: EnderecoData): Promise<boolean> => { const cadastrarEndereco = useCallback(async (formData: EnderecoData): Promise<boolean> => {
setIsCreating(true); setIsCreating(true);
@ -103,12 +96,12 @@ export const useEnderecos = (): UseEnderecosReturn => {
if (response.success) { if (response.success) {
await listarEnderecos(1, searchTerm); await listarEnderecos(1, searchTerm);
return true; return true;
} else {
setError(response.error || 'Erro ao cadastrar endereço');
return false;
} }
} catch (err) {
setError('Erro de conexão ao cadastrar endereço'); setError(response.error || 'Erro ao cadastrar endereco');
return false;
} catch {
setError('Erro de conexao ao cadastrar endereco');
return false; return false;
} finally { } finally {
setIsCreating(false); setIsCreating(false);
@ -125,17 +118,17 @@ export const useEnderecos = (): UseEnderecosReturn => {
if (response.success) { if (response.success) {
await listarEnderecos(currentPage, searchTerm); await listarEnderecos(currentPage, searchTerm);
return true; return true;
} else {
setError(response.error || 'Erro ao atualizar endereço');
return false;
} }
} catch (err) {
setError('Erro de conexão ao atualizar endereço'); setError(response.error || 'Erro ao atualizar endereco');
return false;
} catch {
setError('Erro de conexao ao atualizar endereco');
return false; return false;
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [currentPage, listarEnderecos]); }, [currentPage, listarEnderecos, searchTerm]);
const deletarEndereco = useCallback(async (id: string): Promise<boolean> => { const deletarEndereco = useCallback(async (id: string): Promise<boolean> => {
setLoading(true); setLoading(true);
@ -147,17 +140,17 @@ export const useEnderecos = (): UseEnderecosReturn => {
if (response.success) { if (response.success) {
await listarEnderecos(currentPage, searchTerm); await listarEnderecos(currentPage, searchTerm);
return true; return true;
} else {
setError(response.error || 'Erro ao deletar endereço');
return false;
} }
} catch (err) {
setError('Erro de conexão ao deletar endereço'); setError(response.error || 'Erro ao deletar endereco');
return false;
} catch {
setError('Erro de conexao ao deletar endereco');
return false; return false;
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [currentPage, listarEnderecos]); }, [currentPage, listarEnderecos, searchTerm]);
useEffect(() => { useEffect(() => {
if (isInitialMount.current) { if (isInitialMount.current) {
@ -167,9 +160,10 @@ export const useEnderecos = (): UseEnderecosReturn => {
if (searchTerm.trim()) { if (searchTerm.trim()) {
buscarEnderecos(searchTerm, currentPage); buscarEnderecos(searchTerm, currentPage);
} else { return;
listarEnderecos(currentPage);
} }
listarEnderecos(currentPage);
}, [currentPage, searchTerm, listarEnderecos, buscarEnderecos]); }, [currentPage, searchTerm, listarEnderecos, buscarEnderecos]);
return { return {
@ -190,4 +184,3 @@ export const useEnderecos = (): UseEnderecosReturn => {
setSearchTerm, setSearchTerm,
}; };
}; };

View file

@ -47,6 +47,21 @@ export interface CarrinhoDocument extends LegacyDocument {
quantidades?: number[] | number; quantidades?: number[] | number;
} }
export interface EnderecoDocument extends LegacyDocument {
titulo?: string;
cep?: string;
logradouro?: string;
bairro?: string;
numero?: string;
complemento?: string;
cidade?: string;
estado?: string;
pais?: string;
latitude?: number;
longitude?: number;
principal?: boolean;
}
export interface ServiceResponse<TDocument> { export interface ServiceResponse<TDocument> {
success: boolean; success: boolean;
data?: TDocument | null; data?: TDocument | null;