refactor(saveinmed): type legacy addresses and parameterize bootstrap admin
This commit is contained in:
parent
7e3604ed4a
commit
2829a3e87c
7 changed files with 219 additions and 230 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue