feat(web): redesign login page, fix search bug and enhance seeder
This commit is contained in:
parent
baa60c0d9b
commit
73ad7296ca
4 changed files with 398 additions and 135 deletions
|
|
@ -73,7 +73,7 @@ func seedData(ctx context.Context, db *sqlx.DB, cfg config.Config) {
|
||||||
Email: cfg.AdminEmail,
|
Email: cfg.AdminEmail,
|
||||||
}, cfg.AdminPassword, cfg.PasswordPepper)
|
}, cfg.AdminPassword, cfg.PasswordPepper)
|
||||||
|
|
||||||
// 2. Distributors (Sellers)
|
// 2. Distributors (Sellers - SP Center)
|
||||||
distributor1ID := uuid.Must(uuid.NewV7())
|
distributor1ID := uuid.Must(uuid.NewV7())
|
||||||
createCompany(ctx, db, &domain.Company{
|
createCompany(ctx, db, &domain.Company{
|
||||||
ID: distributor1ID,
|
ID: distributor1ID,
|
||||||
|
|
@ -92,7 +92,28 @@ func seedData(ctx context.Context, db *sqlx.DB, cfg config.Config) {
|
||||||
Username: "distribuidora",
|
Username: "distribuidora",
|
||||||
Email: "distribuidora@saveinmed.com",
|
Email: "distribuidora@saveinmed.com",
|
||||||
}, "123456", cfg.PasswordPepper)
|
}, "123456", cfg.PasswordPepper)
|
||||||
createShippingSettings(ctx, db, distributor1ID)
|
createShippingSettings(ctx, db, distributor1ID, -23.55052, -46.633308, "Rua da Distribuidora, 1000")
|
||||||
|
|
||||||
|
// 2b. Second Distributor (Sellers - SP East Zone/Mooca)
|
||||||
|
distributor2ID := uuid.Must(uuid.NewV7())
|
||||||
|
createCompany(ctx, db, &domain.Company{
|
||||||
|
ID: distributor2ID,
|
||||||
|
CNPJ: "55555555000555",
|
||||||
|
CorporateName: "Distribuidora Leste Ltda",
|
||||||
|
Category: "distribuidora",
|
||||||
|
LicenseNumber: "DIST-002",
|
||||||
|
IsVerified: true,
|
||||||
|
Latitude: -23.55952,
|
||||||
|
Longitude: -46.593308,
|
||||||
|
})
|
||||||
|
createUser(ctx, db, &domain.User{
|
||||||
|
CompanyID: distributor2ID,
|
||||||
|
Role: "Owner",
|
||||||
|
Name: "Gerente Leste",
|
||||||
|
Username: "distribuidora_leste",
|
||||||
|
Email: "leste@saveinmed.com",
|
||||||
|
}, "123456", cfg.PasswordPepper)
|
||||||
|
createShippingSettings(ctx, db, distributor2ID, -23.55952, -46.593308, "Rua da Mooca, 500")
|
||||||
|
|
||||||
// 3. Pharmacies (Buyers)
|
// 3. Pharmacies (Buyers)
|
||||||
pharmacy1ID := uuid.Must(uuid.NewV7())
|
pharmacy1ID := uuid.Must(uuid.NewV7())
|
||||||
|
|
@ -114,46 +135,118 @@ func seedData(ctx context.Context, db *sqlx.DB, cfg config.Config) {
|
||||||
Email: "farmacia@saveinmed.com",
|
Email: "farmacia@saveinmed.com",
|
||||||
}, "123456", cfg.PasswordPepper)
|
}, "123456", cfg.PasswordPepper)
|
||||||
|
|
||||||
|
pharmacy2ID := uuid.Must(uuid.NewV7())
|
||||||
|
createCompany(ctx, db, &domain.Company{
|
||||||
|
ID: pharmacy2ID,
|
||||||
|
CNPJ: "33333333000133",
|
||||||
|
CorporateName: "Drogarias Tiete",
|
||||||
|
Category: "farmacia",
|
||||||
|
LicenseNumber: "FARM-002",
|
||||||
|
IsVerified: true,
|
||||||
|
Latitude: -23.51052,
|
||||||
|
Longitude: -46.613308,
|
||||||
|
})
|
||||||
|
createUser(ctx, db, &domain.User{
|
||||||
|
CompanyID: pharmacy2ID,
|
||||||
|
Role: "Owner",
|
||||||
|
Name: "Gerente Tiete",
|
||||||
|
Username: "farmacia2",
|
||||||
|
Email: "tiete@saveinmed.com",
|
||||||
|
}, "123456", cfg.PasswordPepper)
|
||||||
|
|
||||||
// 4. Products
|
// 4. Products
|
||||||
products := []struct {
|
// List of diverse products
|
||||||
|
commonMeds := []struct {
|
||||||
Name string
|
Name string
|
||||||
Price int64
|
Price int64 // Base price
|
||||||
Stock int64
|
|
||||||
}{
|
}{
|
||||||
{"Dipirona 500mg", 500, 1000},
|
{"Dipirona Sódica 500mg", 450},
|
||||||
{"Paracetamol 750mg", 750, 1000},
|
{"Paracetamol 750mg", 600},
|
||||||
{"Ibuprofeno 600mg", 1200, 500},
|
{"Ibuprofeno 400mg", 1100},
|
||||||
{"Amoxicilina 500mg", 2500, 300},
|
{"Amoxicilina 500mg", 2200},
|
||||||
{"Omeprazol 20mg", 1500, 800},
|
{"Omeprazol 20mg", 1400},
|
||||||
|
{"Simeticona 40mg", 700},
|
||||||
|
{"Dorflex 36cp", 1850},
|
||||||
|
{"Neosaldina 30dg", 2100},
|
||||||
|
{"Torsilax 30cp", 1200},
|
||||||
|
{"Cimegripe 20caps", 1300},
|
||||||
|
{"Losartana Potássica 50mg", 800},
|
||||||
|
{"Atenolol 25mg", 950},
|
||||||
|
{"Metformina 850mg", 750},
|
||||||
|
{"Sildenafila 50mg", 1500},
|
||||||
|
{"Azitromicina 500mg", 2800},
|
||||||
}
|
}
|
||||||
|
|
||||||
var productIDs []uuid.UUID
|
var productIDs []uuid.UUID
|
||||||
|
|
||||||
for _, p := range products {
|
// Seed products for Dist 1
|
||||||
|
for i, p := range commonMeds {
|
||||||
id := uuid.Must(uuid.NewV7())
|
id := uuid.Must(uuid.NewV7())
|
||||||
expiry := time.Now().AddDate(1, 0, 0)
|
expiry := time.Now().AddDate(1, 0, 0)
|
||||||
|
|
||||||
|
// Vary price slightly
|
||||||
|
finalPrice := p.Price + int64(i*10) - 50
|
||||||
|
if finalPrice < 100 {
|
||||||
|
finalPrice = 100
|
||||||
|
}
|
||||||
|
|
||||||
createProduct(ctx, db, &domain.Product{
|
createProduct(ctx, db, &domain.Product{
|
||||||
ID: id,
|
ID: id,
|
||||||
SellerID: distributor1ID,
|
SellerID: distributor1ID,
|
||||||
Name: p.Name,
|
Name: p.Name,
|
||||||
Description: "Medicamento genérico de alta qualidade",
|
Description: "Medicamento genérico de alta qualidade (Nacional)",
|
||||||
Batch: "BATCH-" + id.String()[:8],
|
Batch: "BATCH-NAC-" + id.String()[:4],
|
||||||
ExpiresAt: expiry,
|
ExpiresAt: expiry,
|
||||||
PriceCents: p.Price,
|
PriceCents: finalPrice,
|
||||||
Stock: p.Stock,
|
Stock: 1000 + int64(i*100),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Keep first 5 for orders
|
||||||
|
if i < 5 {
|
||||||
productIDs = append(productIDs, id)
|
productIDs = append(productIDs, id)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed products for Dist 2 (Leste) - Only some of them, different prices
|
||||||
|
for i, p := range commonMeds {
|
||||||
|
if i%2 == 0 {
|
||||||
|
continue
|
||||||
|
} // Skip half
|
||||||
|
|
||||||
|
id := uuid.Must(uuid.NewV7())
|
||||||
|
expiry := time.Now().AddDate(0, 6, 0) // Shorter expiry
|
||||||
|
|
||||||
|
// Cheaper but fewer stock
|
||||||
|
finalPrice := p.Price - 100
|
||||||
|
if finalPrice < 100 {
|
||||||
|
finalPrice = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
createProduct(ctx, db, &domain.Product{
|
||||||
|
ID: id,
|
||||||
|
SellerID: distributor2ID,
|
||||||
|
Name: p.Name,
|
||||||
|
Description: "Distribuição exclusiva ZL",
|
||||||
|
Batch: "BATCH-ZL-" + id.String()[:4],
|
||||||
|
ExpiresAt: expiry,
|
||||||
|
PriceCents: finalPrice,
|
||||||
|
Stock: 50 + int64(i*10),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 5. Orders
|
// 5. Orders
|
||||||
// Create an order from Pharmacy to Distributor
|
// Order 1: Pharmacy 1 buying from Dist 1
|
||||||
orderID := uuid.Must(uuid.NewV7())
|
orderID := uuid.Must(uuid.NewV7())
|
||||||
totalCents := int64(0)
|
totalCents := int64(0)
|
||||||
|
|
||||||
// Items
|
// Items
|
||||||
qty := int64(10)
|
qty := int64(10)
|
||||||
price := products[0].Price
|
priceItem := productIDs[0] // Dipirona from Dist 1
|
||||||
itemTotal := price * qty
|
// We need to fetch price ideally but we know logic... let's just reuse base logic approx or fetch from struct above
|
||||||
|
// Simulating price:
|
||||||
|
unitPrice := commonMeds[0].Price - 50 // Same logic as above: p.Price + 0*10 - 50
|
||||||
|
|
||||||
|
itemTotal := unitPrice * qty
|
||||||
totalCents += itemTotal
|
totalCents += itemTotal
|
||||||
|
|
||||||
createOrder(ctx, db, &domain.Order{
|
createOrder(ctx, db, &domain.Order{
|
||||||
|
|
@ -169,13 +262,25 @@ func seedData(ctx context.Context, db *sqlx.DB, cfg config.Config) {
|
||||||
createOrderItem(ctx, db, &domain.OrderItem{
|
createOrderItem(ctx, db, &domain.OrderItem{
|
||||||
ID: uuid.Must(uuid.NewV7()),
|
ID: uuid.Must(uuid.NewV7()),
|
||||||
OrderID: orderID,
|
OrderID: orderID,
|
||||||
ProductID: productIDs[0],
|
ProductID: priceItem,
|
||||||
Quantity: qty,
|
Quantity: qty,
|
||||||
UnitCents: price,
|
UnitCents: unitPrice,
|
||||||
Batch: "BATCH-" + productIDs[0].String()[:8],
|
Batch: "BATCH-NAC-" + priceItem.String()[:4],
|
||||||
ExpiresAt: time.Now().AddDate(1, 0, 0),
|
ExpiresAt: time.Now().AddDate(1, 0, 0),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Order 2: Pharmacy 2 buying from Dist 1 (Pending)
|
||||||
|
order2ID := uuid.Must(uuid.NewV7())
|
||||||
|
createOrder(ctx, db, &domain.Order{
|
||||||
|
ID: order2ID,
|
||||||
|
BuyerID: pharmacy2ID,
|
||||||
|
SellerID: distributor1ID,
|
||||||
|
Status: "Pendente",
|
||||||
|
TotalCents: 5000,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createCompany(ctx context.Context, db *sqlx.DB, c *domain.Company) {
|
func createCompany(ctx context.Context, db *sqlx.DB, c *domain.Company) {
|
||||||
|
|
@ -221,7 +326,7 @@ func createProduct(ctx context.Context, db *sqlx.DB, p *domain.Product) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createShippingSettings(ctx context.Context, db *sqlx.DB, vendorID uuid.UUID) {
|
func createShippingSettings(ctx context.Context, db *sqlx.DB, vendorID uuid.UUID, lat, lng float64, address string) {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
settings := &domain.ShippingSettings{
|
settings := &domain.ShippingSettings{
|
||||||
VendorID: vendorID,
|
VendorID: vendorID,
|
||||||
|
|
@ -231,12 +336,12 @@ func createShippingSettings(ctx context.Context, db *sqlx.DB, vendorID uuid.UUID
|
||||||
MinFeeCents: 1000, // R$ 10.00
|
MinFeeCents: 1000, // R$ 10.00
|
||||||
FreeShippingThresholdCents: nil,
|
FreeShippingThresholdCents: nil,
|
||||||
PickupActive: true,
|
PickupActive: true,
|
||||||
PickupAddress: "Rua da Distribuidora, 1000",
|
PickupAddress: address,
|
||||||
PickupHours: "Seg-Sex 08-18h",
|
PickupHours: "Seg-Sex 08-18h",
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
Latitude: -23.55052,
|
Latitude: lat,
|
||||||
Longitude: -46.633308,
|
Longitude: lng,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := db.NamedExecContext(ctx, `
|
_, err := db.NamedExecContext(ctx, `
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,18 @@ import { useAuth, UserRole } from '../context/AuthContext'
|
||||||
import { authService } from '../services/auth'
|
import { authService } from '../services/auth'
|
||||||
import { logger } from '../utils/logger'
|
import { logger } from '../utils/logger'
|
||||||
import { decodeJwtPayload } from '../utils/jwt'
|
import { decodeJwtPayload } from '../utils/jwt'
|
||||||
|
import logoImg from '../assets/logo.png' // Ensure logo import is handled
|
||||||
|
|
||||||
// Eye icon components for password visibility toggle
|
// Eye icon components for password visibility toggle
|
||||||
const EyeIcon = () => (
|
const EyeIcon = () => (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 text-gray-500">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const EyeSlashIcon = () => (
|
const EyeSlashIcon = () => (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 text-gray-500">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
@ -26,25 +27,17 @@ export function LoginPage() {
|
||||||
const [showPassword, setShowPassword] = useState(false)
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [activeTab, setActiveTab] = useState<'login' | 'register'>('login')
|
||||||
|
|
||||||
const resolveRole = (role?: string): UserRole => {
|
const resolveRole = (role?: string): UserRole => {
|
||||||
logger.info('🔐 [Login] Resolving role:', role)
|
logger.info('🔐 [Login] Resolving role:', role)
|
||||||
switch (role?.toLowerCase()) {
|
switch (role?.toLowerCase()) {
|
||||||
case 'admin':
|
case 'admin': return 'admin'
|
||||||
return 'admin'
|
case 'dono': return 'owner'
|
||||||
case 'dono':
|
case 'colaborador': return 'employee'
|
||||||
return 'owner'
|
case 'entregador': return 'delivery'
|
||||||
case 'colaborador':
|
case 'customer': return 'customer'
|
||||||
return 'employee'
|
case 'seller': default: return 'seller'
|
||||||
case 'entregador':
|
|
||||||
return 'delivery'
|
|
||||||
case 'customer':
|
|
||||||
return 'customer'
|
|
||||||
case 'seller': // keep legacy
|
|
||||||
default:
|
|
||||||
// Default to seller/owner or log warning?
|
|
||||||
logger.warn('⚠️ [Login] Unknown role, defaulting to seller:', role)
|
|
||||||
return 'seller'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,37 +46,20 @@ export function LoginPage() {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setErrorMessage(null)
|
setErrorMessage(null)
|
||||||
|
|
||||||
logger.info('🔐 [Login] Attempting login with username:', username)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info('🔐 [Login] Calling authService.login...')
|
|
||||||
const response = await authService.login({ username, password })
|
const response = await authService.login({ username, password })
|
||||||
logger.info('🔐 [Login] Response received:', response)
|
|
||||||
|
|
||||||
const { token } = response
|
const { token } = response
|
||||||
logger.debug('🔐 [Login] Token extracted:', token ? `${token.substring(0, 50)}...` : 'NULL/UNDEFINED')
|
if (!token) throw new Error('Resposta de login inválida. Verifique o usuário e a senha.')
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
logger.error('🔐 [Login] ERROR: Token is null or undefined!')
|
|
||||||
throw new Error('Resposta de login inválida. Verifique o usuário e a senha.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = decodeJwtPayload<{ role?: string, sub: string, company_id?: string }>(token)
|
const payload = decodeJwtPayload<{ role?: string, sub: string, company_id?: string }>(token)
|
||||||
logger.debug('🔐 [Login] JWT payload decoded:', payload)
|
|
||||||
|
|
||||||
const role = resolveRole(payload?.role)
|
const role = resolveRole(payload?.role)
|
||||||
logger.info('🔐 [Login] Role resolved:', role)
|
|
||||||
|
|
||||||
login(token, role, username, payload?.sub || '', payload?.company_id, undefined, username)
|
login(token, role, username, payload?.sub || '', payload?.company_id, undefined, username)
|
||||||
logger.info('🔐 [Login] Login successful!')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('🔐 [Login] ERROR caught:', error)
|
|
||||||
const fallback = 'Não foi possível autenticar. Verifique suas credenciais.'
|
const fallback = 'Não foi possível autenticar. Verifique suas credenciais.'
|
||||||
if (axios.isAxiosError(error)) {
|
if (axios.isAxiosError(error)) {
|
||||||
logger.error('🔐 [Login] Axios error response:', error.response?.data)
|
|
||||||
setErrorMessage(error.response?.data?.error ?? fallback)
|
setErrorMessage(error.response?.data?.error ?? fallback)
|
||||||
} else if (error instanceof Error) {
|
} else if (error instanceof Error) {
|
||||||
logger.error('🔐 [Login] Error message:', error.message)
|
|
||||||
setErrorMessage(error.message)
|
setErrorMessage(error.message)
|
||||||
} else {
|
} else {
|
||||||
setErrorMessage(fallback)
|
setErrorMessage(fallback)
|
||||||
|
|
@ -94,63 +70,124 @@ export function LoginPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-gray-100">
|
<div className="flex min-h-screen items-center justify-center bg-gray-100 p-4">
|
||||||
<form
|
<div className="w-full max-w-[400px] overflow-hidden rounded-2xl bg-white shadow-xl">
|
||||||
onSubmit={onSubmit}
|
{/* Blue Header with Logo */}
|
||||||
className="w-full max-w-md space-y-4 rounded-lg bg-white p-8 shadow-lg"
|
<div className="flex flex-col items-center bg-blue-600 pb-8 pt-10 text-white">
|
||||||
|
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-white/10 backdrop-blur-sm">
|
||||||
|
<img src={logoImg} alt="Logo" className="h-10 w-auto brightness-0 invert" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-2xl font-bold">SaveInMed</h1>
|
||||||
|
<p className="text-blue-100 text-sm">Plataforma B2B de Medicamentos</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex border-b border-gray-100 p-2 bg-white">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('login')}
|
||||||
|
className={`flex-1 rounded-lg py-2 text-sm font-medium transition-colors ${activeTab === 'login' ? 'bg-white text-blue-600 shadow-sm border border-gray-100' : 'text-gray-500 hover:text-gray-700'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<h1 className="text-2xl font-bold text-medicalBlue">Acesso ao Marketplace</h1>
|
Entrar
|
||||||
<p className="text-sm text-gray-600">Informe suas credenciais para acessar o marketplace.</p>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('register')}
|
||||||
|
className={`flex-1 rounded-lg py-2 text-sm font-medium transition-colors ${activeTab === 'register' ? 'bg-white text-blue-600 shadow-sm border border-gray-100' : 'text-gray-500 hover:text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Cadastrar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Login Form */}
|
||||||
|
{activeTab === 'login' ? (
|
||||||
|
<form onSubmit={onSubmit} className="p-8 space-y-5">
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<div className="rounded border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
|
<div className="flex items-center gap-2 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-600 border border-red-100">
|
||||||
{errorMessage}
|
<svg className="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<span>credenciais inválidas, tente novamente</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium text-gray-700" htmlFor="username">
|
<div className="space-y-1.5">
|
||||||
Usuário
|
<label className="text-sm font-medium text-gray-700 ml-1">Email ou Usuário</label>
|
||||||
</label>
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-gray-400">
|
||||||
|
<span className="text-lg">@</span>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
id="username"
|
id="username"
|
||||||
name="username"
|
name="username"
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
className="w-full rounded border border-gray-200 px-3 py-2"
|
placeholder="seu@email.com"
|
||||||
|
className="w-full rounded-xl border border-gray-200 py-2.5 pl-10 pr-3 text-gray-800 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
</div>
|
||||||
<label className="text-sm font-medium text-gray-700" htmlFor="password">
|
|
||||||
Senha
|
<div className="space-y-1.5">
|
||||||
</label>
|
<label className="text-sm font-medium text-gray-700 ml-1">Senha</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-gray-400">
|
||||||
|
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
className="w-full rounded border border-gray-200 px-3 py-2 pr-10"
|
placeholder="••••••••"
|
||||||
|
className="w-full rounded-xl border border-gray-200 py-2.5 pl-10 pr-10 text-gray-800 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700 focus:outline-none"
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none transition-colors"
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
aria-label={showPassword ? "Ocultar senha" : "Mostrar senha"}
|
|
||||||
>
|
>
|
||||||
{showPassword ? <EyeSlashIcon /> : <EyeIcon />}
|
{showPassword ? <EyeSlashIcon /> : <EyeIcon />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full rounded bg-healthGreen px-4 py-2 font-semibold text-white disabled:cursor-not-allowed disabled:opacity-70"
|
className="w-full rounded-xl bg-slate-600 py-3 font-semibold text-white shadow-lg shadow-slate-200 hover:bg-slate-700 hover:shadow-xl active:translate-y-0.5 focus:ring-2 focus:ring-slate-200 transition-all disabled:opacity-70 flex items-center justify-center gap-2"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? 'Entrando...' : 'Entrar'}
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<svg className="animate-spin h-5 w-5 text-white" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
|
||||||
|
Entrando...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1" /></svg>
|
||||||
|
Entrar
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
) : (
|
||||||
|
<div className="p-8 text-center text-gray-500 py-12">
|
||||||
|
<p>Funcionalidade de cadastro em breve.</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('login')}
|
||||||
|
className="mt-4 text-blue-600 hover:underline text-sm"
|
||||||
|
>
|
||||||
|
Voltar para login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,150 @@
|
||||||
|
import { useState } from 'react'
|
||||||
import { Shell } from '../layouts/Shell'
|
import { Shell } from '../layouts/Shell'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
|
import { adminService } from '../services/adminService'
|
||||||
|
|
||||||
export function MyProfilePage() {
|
export function MyProfilePage() {
|
||||||
const { user } = useAuth()
|
const { user, login } = useAuth()
|
||||||
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: user?.name || '',
|
||||||
|
email: user?.email || '',
|
||||||
|
username: user?.username || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFormData({ ...formData, [e.target.name]: e.target.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!user) return
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const updatedUser = await adminService.updateUser(user.id, {
|
||||||
|
name: formData.name,
|
||||||
|
email: formData.email,
|
||||||
|
username: formData.username
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update local auth context
|
||||||
|
login(
|
||||||
|
user.token,
|
||||||
|
user.role,
|
||||||
|
updatedUser.name,
|
||||||
|
updatedUser.id,
|
||||||
|
updatedUser.company_id,
|
||||||
|
updatedUser.email,
|
||||||
|
updatedUser.username
|
||||||
|
)
|
||||||
|
|
||||||
|
setIsEditing(false)
|
||||||
|
alert('Perfil atualizado com sucesso!')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error updating profile', err)
|
||||||
|
alert('Erro ao atualizar perfil. Tente novamente.')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell>
|
<Shell>
|
||||||
<div className="space-y-6 rounded-lg bg-white p-6 shadow-sm">
|
<div className="space-y-6 rounded-lg bg-white p-6 shadow-sm max-w-4xl mx-auto">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-semibold text-medicalBlue">Meu Perfil</h1>
|
<h1 className="text-xl font-semibold text-medicalBlue">Meu Perfil</h1>
|
||||||
<p className="text-sm text-gray-600">Acompanhe as informações básicas da sua conta.</p>
|
<p className="text-sm text-gray-600">Acompanhe as informações básicas da sua conta.</p>
|
||||||
</div>
|
</div>
|
||||||
|
{!isEditing ? (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsEditing(true)}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Editar Dados
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditing(false)
|
||||||
|
setFormData({
|
||||||
|
name: user?.name || '',
|
||||||
|
email: user?.email || '',
|
||||||
|
username: user?.username || ''
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSave} className="grid gap-6">
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<div className="rounded-md border border-gray-200 p-4">
|
<div className="rounded-md border border-gray-200 p-4 relative">
|
||||||
<p className="text-xs uppercase text-gray-400">Nome</p>
|
<label className="text-xs uppercase text-gray-400 block mb-1">Nome</label>
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full p-1 border-b border-blue-500 focus:outline-none text-sm font-semibold text-gray-800"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<p className="text-sm font-semibold text-gray-800">{user?.name ?? 'Não informado'}</p>
|
<p className="text-sm font-semibold text-gray-800">{user?.name ?? 'Não informado'}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border border-gray-200 p-4">
|
|
||||||
<p className="text-xs uppercase text-gray-400">Perfil</p>
|
<div className="rounded-md border border-gray-200 p-4 bg-gray-50">
|
||||||
|
<p className="text-xs uppercase text-gray-400 mb-1">Perfil</p>
|
||||||
<p className="text-sm font-semibold text-gray-800">{user?.role ?? 'Não informado'}</p>
|
<p className="text-sm font-semibold text-gray-800">{user?.role ?? 'Não informado'}</p>
|
||||||
|
{isEditing && <span className="text-[10px] text-gray-500 absolute top-2 right-2">(Não editável)</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-md border border-gray-200 p-4">
|
<div className="rounded-md border border-gray-200 p-4">
|
||||||
<p className="text-xs uppercase text-gray-400">E-mail</p>
|
<label className="text-xs uppercase text-gray-400 block mb-1">E-mail</label>
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full p-1 border-b border-blue-500 focus:outline-none text-sm font-semibold text-gray-800"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<p className="text-sm font-semibold text-gray-800">{user?.email ?? 'Não informado'}</p>
|
<p className="text-sm font-semibold text-gray-800">{user?.email ?? 'Não informado'}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-md border border-gray-200 p-4">
|
<div className="rounded-md border border-gray-200 p-4">
|
||||||
<p className="text-xs uppercase text-gray-400">Usuário</p>
|
<label className="text-xs uppercase text-gray-400 block mb-1">Usuário</label>
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
name="username"
|
||||||
|
value={formData.username}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full p-1 border-b border-blue-500 focus:outline-none text-sm font-semibold text-gray-800"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<p className="text-sm font-semibold text-gray-800">{user?.username ?? 'Não informado'}</p>
|
<p className="text-sm font-semibold text-gray-800">{user?.username ?? 'Não informado'}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isEditing && (
|
||||||
|
<div className="flex justify-end pt-4 border-t">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="bg-emerald-600 hover:bg-emerald-700 text-white px-6 py-2 rounded-lg font-medium shadow-sm disabled:opacity-50 transition-all flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{loading ? 'Salvando...' : 'Salvar Alterações'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Shell>
|
</Shell>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,11 @@ const ProductSearch = () => {
|
||||||
}
|
}
|
||||||
}, [products, user?.id, sortBy])
|
}, [products, user?.id, sortBy])
|
||||||
|
|
||||||
|
// Calculate total visible offers after filtering
|
||||||
|
const visibleOffers = useMemo(() => {
|
||||||
|
return groupedProducts.reduce((acc, group) => acc + group.offerCount, 0)
|
||||||
|
}, [groupedProducts])
|
||||||
|
|
||||||
const handleLocationSelect = (newLat: number, newLng: number) => {
|
const handleLocationSelect = (newLat: number, newLng: number) => {
|
||||||
setLat(newLat)
|
setLat(newLat)
|
||||||
setLng(newLng)
|
setLng(newLng)
|
||||||
|
|
@ -261,7 +266,7 @@ const ProductSearch = () => {
|
||||||
<div className="lg:col-span-3">
|
<div className="lg:col-span-3">
|
||||||
<div className="mb-4 flex justify-between items-center">
|
<div className="mb-4 flex justify-between items-center">
|
||||||
<h2 className="text-xl font-semibold text-gray-700">
|
<h2 className="text-xl font-semibold text-gray-700">
|
||||||
{loading ? 'Buscando...' : `${groupedProducts.length} medicamentos (${total} ofertas)`}
|
{loading ? 'Buscando...' : `${groupedProducts.length} medicamentos (${visibleOffers} ofertas)`}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<span className="text-gray-500">Ordenar por:</span>
|
<span className="text-gray-500">Ordenar por:</span>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue