From 2829a3e87c064a4e193812e18b7ef66c898cd2fa Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Sat, 7 Mar 2026 08:24:53 -0600 Subject: [PATCH] refactor(saveinmed): type legacy addresses and parameterize bootstrap admin --- backend/internal/config/config.go | 4 + backend/internal/server/server.go | 53 ++----- frontend/src/app/enderecos/page.tsx | 171 ++++++++++++----------- frontend/src/components/EnderecoForm.tsx | 51 ++++--- frontend/src/components/EnderecoList.tsx | 46 +++--- frontend/src/hooks/useEnderecos.ts | 109 +++++++-------- frontend/src/types/legacyEntities.ts | 15 ++ 7 files changed, 219 insertions(+), 230 deletions(-) diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 83af525..899df00 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -27,6 +27,8 @@ type Config struct { SwaggerSchemes []string MercadoPagoPublicKey string MapboxAccessToken string + BootstrapAdminEmail string + BootstrapAdminPassword string } // Load reads configuration from environment variables and applies sane defaults @@ -50,6 +52,8 @@ func Load() (*Config, error) { SwaggerSchemes: getEnvStringSlice("SWAGGER_SCHEMES", []string{"http"}), MercadoPagoPublicKey: getEnv("MERCADOPAGO_PUBLIC_KEY", "TEST-PUBLIC-KEY"), MapboxAccessToken: getEnv("MAPBOX_ACCESS_TOKEN", ""), + BootstrapAdminEmail: getEnv("BOOTSTRAP_ADMIN_EMAIL", "admin@saveinmed.com.br"), + BootstrapAdminPassword: getEnv("BOOTSTRAP_ADMIN_PASSWORD", "sim-admin"), } return &cfg, nil diff --git a/backend/internal/server/server.go b/backend/internal/server/server.go index b4fe94f..18298a1 100644 --- a/backend/internal/server/server.go +++ b/backend/internal/server/server.go @@ -25,8 +25,6 @@ import ( const ( bootstrapAdminName = "SaveInMed Admin" bootstrapAdminUsername = "admin" - bootstrapAdminEmail = "admin@saveinmed.com" - bootstrapAdminPassword = "sim-admin" ) // Server wires the infrastructure and exposes HTTP handlers. @@ -223,15 +221,16 @@ func (s *Server) Start(ctx context.Context) error { // Seed platform admin automatically { - existingUser, err := repo.GetUserByEmail(ctx, bootstrapAdminEmail) - if err != nil { - log.Printf("Seeding admin user: %s", bootstrapAdminEmail) + adminEmail := s.cfg.BootstrapAdminEmail + adminPassword := s.cfg.BootstrapAdminPassword + + 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{ ID: uuid.Nil, - CNPJ: adminCNPJ, + CNPJ: "00000000000000", CorporateName: "SaveInMed Platform", Category: "platform", LicenseNumber: "ADMIN", @@ -240,46 +239,21 @@ func (s *Server) Start(ctx context.Context) error { 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{ Role: domain.RoleAdmin, Name: bootstrapAdminName, Username: bootstrapAdminUsername, - Email: bootstrapAdminEmail, + Email: adminEmail, Superadmin: false, - }, bootstrapAdminPassword) + }, adminPassword) if err != nil { log.Printf("Failed to seed admin: %v", err) } else { - // FORCE VERIFY the platform company if _, err := s.svc.VerifyCompany(ctx, company.ID); err != nil { log.Printf("Failed to verify platform company: %v", err) } - log.Printf("Admin user created successfully") - log.Printf("Bootstrap admin credentials: email=%s password=%s", bootstrapAdminEmail, bootstrapAdminPassword) + log.Printf("Admin user created: email=%s", adminEmail) } } else { existingUser.Role = domain.RoleAdmin @@ -291,11 +265,10 @@ func (s *Server) Start(ctx context.Context) error { existingUser.Username = bootstrapAdminUsername } - if err := s.svc.UpdateUser(ctx, existingUser, bootstrapAdminPassword); err != nil { - log.Printf("Failed to reconcile existing user %s as admin: %v", bootstrapAdminEmail, err) + if err := s.svc.UpdateUser(ctx, existingUser, adminPassword); err != nil { + log.Printf("Failed to reconcile existing user %s as admin: %v", adminEmail, err) } else { - log.Printf("Existing user %s reconciled as admin with bootstrap password", bootstrapAdminEmail) - log.Printf("Bootstrap admin credentials: email=%s password=%s", bootstrapAdminEmail, bootstrapAdminPassword) + log.Printf("Admin user reconciled: email=%s", adminEmail) } } } diff --git a/frontend/src/app/enderecos/page.tsx b/frontend/src/app/enderecos/page.tsx index 7b788b2..1ea3fa4 100644 --- a/frontend/src/app/enderecos/page.tsx +++ b/frontend/src/app/enderecos/page.tsx @@ -1,9 +1,8 @@ -'use client'; +'use client'; import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; -import { getCurrentUserWithRetry, account } from '@/lib/appwrite'; -import { Models } from '@/lib/appwrite'; +import { getCurrentUserWithRetry, Models } from '@/lib/appwrite'; import Header from '@/components/Header'; import { MapPin, Plus } from 'lucide-react'; import EnderecoForm from '@/components/EnderecoForm'; @@ -11,13 +10,15 @@ import EnderecoList from '@/components/EnderecoList'; import { useEnderecos, EnderecoData } from '@/hooks/useEnderecos'; import { RoleGuard } from '@/components/auth/RoleGuard'; import { UserRole } from '@/types/auth'; +import { EnderecoDocument } from '@/types/legacyEntities'; const GestaoEnderecos = () => { const router = useRouter(); const [user, setUser] = useState | null>(null); - const [editing, setEditing] = useState(null); + const [editing, setEditing] = useState(null); const [activeTab, setActiveTab] = useState<'lista' | 'cadastro'>('lista'); const [searchTerm, setSearchTerm] = useState(''); + const [checkingAuth, setCheckingAuth] = useState(true); const { enderecos, @@ -32,21 +33,27 @@ const GestaoEnderecos = () => { cadastrarEndereco, atualizarEndereco, deletarEndereco, - setCurrentPage + setCurrentPage, } = useEnderecos(); useEffect(() => { const initializeUser = async () => { try { - const currentUser = await account.get(); - setUser(currentUser); + const currentUser = await getCurrentUserWithRetry(); + if (!currentUser) { + router.push('/'); + return; + } + setUser(currentUser); if (activeTab === 'lista') { await listarEnderecos(); } } catch (error) { - console.error('❌ Usuário não autenticado:', error); + console.error('Usuario nao autenticado:', error); router.push('/'); + } finally { + setCheckingAuth(false); } }; @@ -61,16 +68,16 @@ const GestaoEnderecos = () => { setActiveTab('lista'); } 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); setActiveTab('cadastro'); }; @@ -90,9 +97,10 @@ const GestaoEnderecos = () => { setSearchTerm(term); if (term.trim()) { await buscarEnderecos(term, 1); - } else { - await listarEnderecos(1); + return; } + + await listarEnderecos(1); }; const handleNextPage = () => { @@ -102,12 +110,12 @@ const GestaoEnderecos = () => { } }; - if (!user) { + if (checkingAuth || !user) { return (
-

Verificando autenticação...

+

Verificando autenticacao...

); @@ -118,72 +126,72 @@ const GestaoEnderecos = () => {
-
-
- -
+
+
+ +
- {activeTab === 'lista' && ( - - searchTerm.trim() - ? buscarEnderecos(searchTerm, currentPage) - : listarEnderecos(currentPage) - } - onPrevPage={handlePrevPage} - onNextPage={handleNextPage} - onSearch={handleSearch} - /> - )} + {activeTab === 'lista' && ( + + searchTerm.trim() + ? buscarEnderecos(searchTerm, currentPage) + : listarEnderecos(currentPage) + } + onPrevPage={handlePrevPage} + onNextPage={handleNextPage} + onSearch={handleSearch} + /> + )} - {activeTab === 'cadastro' && ( - - )} + {activeTab === 'cadastro' && ( + + )}
@@ -191,4 +199,3 @@ const GestaoEnderecos = () => { }; export default GestaoEnderecos; - diff --git a/frontend/src/components/EnderecoForm.tsx b/frontend/src/components/EnderecoForm.tsx index 1f10651..c8c9b99 100644 --- a/frontend/src/components/EnderecoForm.tsx +++ b/frontend/src/components/EnderecoForm.tsx @@ -1,16 +1,14 @@ -import React, { useEffect, useState } from 'react'; -import { Models } from '@/lib/appwrite'; +import React, { useEffect, useState } from 'react'; import { EnderecoData } from '@/services/enderecoService'; +import { EnderecoDocument } from '@/types/legacyEntities'; interface EnderecoFormProps { onSubmit: (data: EnderecoData) => Promise; onCancel?: () => void; - initialData?: Models.Document | null; + initialData?: EnderecoDocument | null; loading?: boolean; } -type EnderecoDocument = Models.Document & EnderecoData; - const buildEmptyEndereco = (): EnderecoData => ({ titulo: '', cep: '', @@ -42,19 +40,18 @@ const EnderecoForm: React.FC = ({ return; } - const document = initialData as EnderecoDocument; setFormData({ - titulo: document.titulo || '', - cep: document.cep || '', - logradouro: document.logradouro || '', - bairro: document.bairro || '', - numero: document.numero || '', - complemento: document.complemento || '', - cidade: document.cidade || '', - estado: document.estado || '', - pais: document.pais || '', - latitude: Number(document.latitude) || 0, - longitude: Number(document.longitude) || 0, + titulo: initialData.titulo || '', + cep: initialData.cep || '', + logradouro: initialData.logradouro || '', + bairro: initialData.bairro || '', + numero: initialData.numero || '', + complemento: initialData.complemento || '', + cidade: initialData.cidade || '', + estado: initialData.estado || '', + pais: initialData.pais || '', + latitude: Number(initialData.latitude) || 0, + longitude: Number(initialData.longitude) || 0, }); }, [initialData]); @@ -68,7 +65,7 @@ const EnderecoForm: React.FC = ({ try { const res = await fetch(`https://lernu-backend-dev.rede5.com.br/viacep/${digits}`); if (!res.ok) { - throw new Error('CEP inválido'); + throw new Error('CEP invalido'); } const data = (await res.json()) as Partial; @@ -83,7 +80,7 @@ const EnderecoForm: React.FC = ({ setCepError(null); } catch (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 = ({ if (success) { setMessage({ 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) { @@ -111,7 +108,7 @@ const EnderecoForm: React.FC = ({ setMessage({ 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 = ({

- {initialData ? 'Editar Endereço' : 'Novo Endereço'} + {initialData ? 'Editar Endereco' : 'Novo Endereco'}

- {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.'}

@@ -170,7 +167,7 @@ const EnderecoForm: React.FC = ({
= ({
= ({
= ({ ); }; -export default EnderecoForm; \ No newline at end of file +export default EnderecoForm; diff --git a/frontend/src/components/EnderecoList.tsx b/frontend/src/components/EnderecoList.tsx index 49ab360..5c45758 100644 --- a/frontend/src/components/EnderecoList.tsx +++ b/frontend/src/components/EnderecoList.tsx @@ -1,21 +1,21 @@ -import React from 'react'; -import { Models } from '@/lib/appwrite'; +import React from 'react'; import SearchBar from './SearchBar'; import RefreshButton from './RefreshButton'; import ListHeader from './ListHeader'; import DataTable, { Column } from './DataTable'; import Pagination from './Pagination'; import TableActions from './TableActions'; +import { EnderecoDocument } from '@/types/legacyEntities'; interface EnderecoListProps { - enderecos: Models.Document[]; + enderecos: EnderecoDocument[]; loading: boolean; isChangingPage?: boolean; error: string | null; totalEnderecos: number; currentPage: number; pageSize: number; - onEdit: (endereco: Models.Document) => void; + onEdit: (endereco: EnderecoDocument) => void; onDelete: (id: string) => Promise; onRefresh: () => void; onPrevPage: () => void; @@ -23,6 +23,14 @@ interface EnderecoListProps { onSearch: (termo: string) => void; } +const columns: Column[] = [ + { key: 'titulo', header: 'Titulo' }, + { key: 'cep', header: 'CEP' }, + { key: 'cidade', header: 'Cidade' }, + { key: 'estado', header: 'Estado' }, + { key: 'pais', header: 'Pais' }, +]; + const EnderecoList: React.FC = ({ enderecos, loading, @@ -36,7 +44,7 @@ const EnderecoList: React.FC = ({ onRefresh, onPrevPage, onNextPage, - onSearch + onSearch, }) => { const totalPages = Math.ceil(totalEnderecos / pageSize); const startItem = (currentPage - 1) * pageSize + 1; @@ -49,27 +57,19 @@ const EnderecoList: React.FC = ({ }; 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); } }; - const columns: Column[] = [ - { key: 'titulo', header: 'Título' }, - { key: 'cep', header: 'CEP' }, - { key: 'cidade', header: 'Cidade' }, - { key: 'estado', header: 'Estado' }, - { key: 'pais', header: 'País' }, - ]; - return (
- + @@ -84,24 +84,24 @@ const EnderecoList: React.FC = ({
- Carregando página... + Carregando pagina...
)} {loading ? (
-
Carregando endereços...
+
Carregando enderecos...
) : ( <> ( + actions={(endereco) => ( onEdit(end)} - onDelete={() => handleDelete(end.$id)} + onEdit={() => onEdit(endereco)} + onDelete={() => handleDelete(endereco.$id)} /> )} /> @@ -109,9 +109,9 @@ const EnderecoList: React.FC = ({
{totalEnderecos > 0 ? ( - <>Mostrando {startItem} - {endItem} de {totalEnderecos} endereços + <>Mostrando {startItem} - {endItem} de {totalEnderecos} enderecos ) : ( - 'Total de endereços: 0' + 'Total de enderecos: 0' )}
diff --git a/frontend/src/hooks/useEnderecos.ts b/frontend/src/hooks/useEnderecos.ts index 151143c..74aef45 100644 --- a/frontend/src/hooks/useEnderecos.ts +++ b/frontend/src/hooks/useEnderecos.ts @@ -1,11 +1,10 @@ -// @ts-nocheck import { useState, useCallback, useRef, useEffect } from 'react'; -import { Models } from '@/lib/appwrite'; import { enderecoService, EnderecoData } from '@/services/enderecoService'; +import { EnderecoDocument } from '@/types/legacyEntities'; export type { EnderecoData }; export interface UseEnderecosReturn { - enderecos: Models.Document[]; + enderecos: EnderecoDocument[]; loading: boolean; isChangingPage: boolean; isCreating: boolean; @@ -25,7 +24,7 @@ export interface UseEnderecosReturn { const PAGE_SIZE = 10; export const useEnderecos = (): UseEnderecosReturn => { - const [enderecos, setEnderecos] = useState([]); + const [enderecos, setEnderecos] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [totalEnderecos, setTotalEnderecos] = useState(0); @@ -35,8 +34,7 @@ export const useEnderecos = (): UseEnderecosReturn => { const [searchTerm, setSearchTerm] = useState(''); const isInitialMount = useRef(true); - const listarEnderecos = useCallback( - async (page = currentPage, search = searchTerm) => { + const listarEnderecos = useCallback(async (page = currentPage, search = searchTerm) => { setIsChangingPage(true); setLoading(true); setError(null); @@ -47,51 +45,46 @@ export const useEnderecos = (): UseEnderecosReturn => { : await enderecoService.listar(page, PAGE_SIZE); if (response.success) { - setEnderecos(response.documents || []); + setEnderecos((response.documents || []) as EnderecoDocument[]); setTotalEnderecos(response.total || 0); setCurrentPage(page); setSearchTerm(search); - } else { - setError(response.error || 'Erro ao carregar endereços'); + return; } - } 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 { setLoading(false); setIsChangingPage(false); } }, [currentPage, searchTerm]); - const buscarEnderecos = useCallback( - async (termo: string, page = 1) => { - setIsChangingPage(true); - setLoading(true); - setError(null); + const buscarEnderecos = useCallback(async (termo: string, page = 1) => { + setIsChangingPage(true); + setLoading(true); + setError(null); - try { - const response = await enderecoService.buscar( - termo, - page, - PAGE_SIZE - ); + try { + const response = await enderecoService.buscar(termo, page, PAGE_SIZE); - if (response.success) { - setEnderecos(response.documents || []); - setTotalEnderecos(response.total || 0); - setCurrentPage(page); - setSearchTerm(termo); - } else { - setError(response.error || 'Erro ao buscar enderecos'); - } - } catch (err) { - setError('Erro de conexão ao buscar enderecos'); - } finally { - setLoading(false); - setIsChangingPage(false); + if (response.success) { + setEnderecos((response.documents || []) as EnderecoDocument[]); + setTotalEnderecos(response.total || 0); + setCurrentPage(page); + setSearchTerm(termo); + return; } - }, - [] - ); + + 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 => { setIsCreating(true); @@ -103,12 +96,12 @@ export const useEnderecos = (): UseEnderecosReturn => { if (response.success) { await listarEnderecos(1, searchTerm); 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; } finally { setIsCreating(false); @@ -125,17 +118,17 @@ export const useEnderecos = (): UseEnderecosReturn => { if (response.success) { await listarEnderecos(currentPage, searchTerm); 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; } finally { setLoading(false); } - }, [currentPage, listarEnderecos]); + }, [currentPage, listarEnderecos, searchTerm]); const deletarEndereco = useCallback(async (id: string): Promise => { setLoading(true); @@ -147,17 +140,17 @@ export const useEnderecos = (): UseEnderecosReturn => { if (response.success) { await listarEnderecos(currentPage, searchTerm); 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; } finally { setLoading(false); } - }, [currentPage, listarEnderecos]); + }, [currentPage, listarEnderecos, searchTerm]); useEffect(() => { if (isInitialMount.current) { @@ -167,9 +160,10 @@ export const useEnderecos = (): UseEnderecosReturn => { if (searchTerm.trim()) { buscarEnderecos(searchTerm, currentPage); - } else { - listarEnderecos(currentPage); + return; } + + listarEnderecos(currentPage); }, [currentPage, searchTerm, listarEnderecos, buscarEnderecos]); return { @@ -190,4 +184,3 @@ export const useEnderecos = (): UseEnderecosReturn => { setSearchTerm, }; }; - diff --git a/frontend/src/types/legacyEntities.ts b/frontend/src/types/legacyEntities.ts index e10bf05..ebbfeeb 100644 --- a/frontend/src/types/legacyEntities.ts +++ b/frontend/src/types/legacyEntities.ts @@ -47,6 +47,21 @@ export interface CarrinhoDocument extends LegacyDocument { 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 { success: boolean; data?: TDocument | null;