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
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

View file

@ -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)
}
}
}

View file

@ -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<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 [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 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 (
<div className="min-h-screen bg-gray-50 flex items-center justify-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>
<p className="text-gray-600">Verificando autenticação...</p>
<p className="text-gray-600">Verificando autenticacao...</p>
</div>
</div>
);
@ -118,8 +126,8 @@ const GestaoEnderecos = () => {
<div className="min-h-screen bg-gray-50">
<Header
user={user}
title="Gestão de Endereços"
subtitle="Gerencie os endereços da plataforma SaveInMed"
title="Gestao de Enderecos"
subtitle="Gerencie os enderecos da plataforma SaveInMed"
/>
<main className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
@ -136,7 +144,7 @@ const GestaoEnderecos = () => {
: '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
onClick={() => {
@ -149,7 +157,7 @@ const GestaoEnderecos = () => {
: '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>
</nav>
</div>
@ -191,4 +199,3 @@ const GestaoEnderecos = () => {
};
export default GestaoEnderecos;

View file

@ -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<boolean>;
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<EnderecoFormProps> = ({
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<EnderecoFormProps> = ({
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<EnderecoData>;
@ -83,7 +80,7 @@ const EnderecoForm: React.FC<EnderecoFormProps> = ({
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<EnderecoFormProps> = ({
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<EnderecoFormProps> = ({
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<EnderecoFormProps> = ({
<div className="bg-white rounded-lg shadow-md p-6">
<div className="mb-6">
<h2 className="text-2xl font-bold text-gray-900 mb-2">
{initialData ? 'Editar Endereço' : 'Novo Endereço'}
{initialData ? 'Editar Endereco' : 'Novo Endereco'}
</h2>
<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>
</div>
@ -170,7 +167,7 @@ const EnderecoForm: React.FC<EnderecoFormProps> = ({
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="titulo" className="block text-sm font-medium text-gray-700 mb-2">
Título *
Titulo *
</label>
<input
type="text"
@ -237,7 +234,7 @@ const EnderecoForm: React.FC<EnderecoFormProps> = ({
</div>
<div>
<label htmlFor="numero" className="block text-sm font-medium text-gray-700 mb-2">
Número *
Numero *
</label>
<input
type="text"
@ -301,7 +298,7 @@ const EnderecoForm: React.FC<EnderecoFormProps> = ({
</div>
<div>
<label htmlFor="pais" className="block text-sm font-medium text-gray-700 mb-2">
País *
Pais *
</label>
<input
type="text"

View file

@ -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<boolean>;
onRefresh: () => void;
onPrevPage: () => void;
@ -23,6 +23,14 @@ interface EnderecoListProps {
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> = ({
enderecos,
loading,
@ -36,7 +44,7 @@ const EnderecoList: React.FC<EnderecoListProps> = ({
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<EnderecoListProps> = ({
};
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<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 (
<div className="container mx-auto px-4 py-8">
<ListHeader title="Lista de Endereços">
<ListHeader title="Lista de Enderecos">
<SearchBar
value={search}
onChange={setSearch}
onSearch={handleSearch}
placeholder="Buscar título, CEP ou cidade"
placeholder="Buscar titulo, CEP ou cidade"
/>
<RefreshButton onClick={onRefresh} loading={loading} />
</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="flex items-center">
<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>
)}
{loading ? (
<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>
) : (
<>
<DataTable
columns={columns}
data={enderecos}
actions={(end) => (
actions={(endereco) => (
<TableActions
onEdit={() => onEdit(end)}
onDelete={() => handleDelete(end.$id)}
onEdit={() => onEdit(endereco)}
onDelete={() => handleDelete(endereco.$id)}
/>
)}
/>
@ -109,9 +109,9 @@ const EnderecoList: React.FC<EnderecoListProps> = ({
<div className="mt-4 flex justify-between items-center">
<div className="text-sm text-gray-600">
{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>

View file

@ -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<Models.Document[]>([]);
const [enderecos, setEnderecos] = useState<EnderecoDocument[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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) => {
const buscarEnderecos = useCallback(async (termo: string, page = 1) => {
setIsChangingPage(true);
setLoading(true);
setError(null);
try {
const response = await enderecoService.buscar(
termo,
page,
PAGE_SIZE
);
const response = await enderecoService.buscar(termo, page, PAGE_SIZE);
if (response.success) {
setEnderecos(response.documents || []);
setEnderecos((response.documents || []) as EnderecoDocument[]);
setTotalEnderecos(response.total || 0);
setCurrentPage(page);
setSearchTerm(termo);
} else {
setError(response.error || 'Erro ao buscar enderecos');
return;
}
} catch (err) {
setError('Erro de conexão ao buscar enderecos');
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> => {
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<boolean> => {
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,
};
};

View file

@ -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<TDocument> {
success: boolean;
data?: TDocument | null;