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,
|
||||
}, cfg.AdminPassword, cfg.PasswordPepper)
|
||||
|
||||
// 2. Distributors (Sellers)
|
||||
// 2. Distributors (Sellers - SP Center)
|
||||
distributor1ID := uuid.Must(uuid.NewV7())
|
||||
createCompany(ctx, db, &domain.Company{
|
||||
ID: distributor1ID,
|
||||
|
|
@ -92,7 +92,28 @@ func seedData(ctx context.Context, db *sqlx.DB, cfg config.Config) {
|
|||
Username: "distribuidora",
|
||||
Email: "distribuidora@saveinmed.com",
|
||||
}, "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)
|
||||
pharmacy1ID := uuid.Must(uuid.NewV7())
|
||||
|
|
@ -114,46 +135,118 @@ func seedData(ctx context.Context, db *sqlx.DB, cfg config.Config) {
|
|||
Email: "farmacia@saveinmed.com",
|
||||
}, "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
|
||||
products := []struct {
|
||||
// List of diverse products
|
||||
commonMeds := []struct {
|
||||
Name string
|
||||
Price int64
|
||||
Stock int64
|
||||
Price int64 // Base price
|
||||
}{
|
||||
{"Dipirona 500mg", 500, 1000},
|
||||
{"Paracetamol 750mg", 750, 1000},
|
||||
{"Ibuprofeno 600mg", 1200, 500},
|
||||
{"Amoxicilina 500mg", 2500, 300},
|
||||
{"Omeprazol 20mg", 1500, 800},
|
||||
{"Dipirona Sódica 500mg", 450},
|
||||
{"Paracetamol 750mg", 600},
|
||||
{"Ibuprofeno 400mg", 1100},
|
||||
{"Amoxicilina 500mg", 2200},
|
||||
{"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
|
||||
|
||||
for _, p := range products {
|
||||
// Seed products for Dist 1
|
||||
for i, p := range commonMeds {
|
||||
id := uuid.Must(uuid.NewV7())
|
||||
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{
|
||||
ID: id,
|
||||
SellerID: distributor1ID,
|
||||
Name: p.Name,
|
||||
Description: "Medicamento genérico de alta qualidade",
|
||||
Batch: "BATCH-" + id.String()[:8],
|
||||
Description: "Medicamento genérico de alta qualidade (Nacional)",
|
||||
Batch: "BATCH-NAC-" + id.String()[:4],
|
||||
ExpiresAt: expiry,
|
||||
PriceCents: p.Price,
|
||||
Stock: p.Stock,
|
||||
PriceCents: finalPrice,
|
||||
Stock: 1000 + int64(i*100),
|
||||
})
|
||||
|
||||
// Keep first 5 for orders
|
||||
if i < 5 {
|
||||
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),
|
||||
})
|
||||
productIDs = append(productIDs, id)
|
||||
}
|
||||
|
||||
// 5. Orders
|
||||
// Create an order from Pharmacy to Distributor
|
||||
// Order 1: Pharmacy 1 buying from Dist 1
|
||||
orderID := uuid.Must(uuid.NewV7())
|
||||
totalCents := int64(0)
|
||||
|
||||
// Items
|
||||
qty := int64(10)
|
||||
price := products[0].Price
|
||||
itemTotal := price * qty
|
||||
priceItem := productIDs[0] // Dipirona from Dist 1
|
||||
// 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
|
||||
|
||||
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{
|
||||
ID: uuid.Must(uuid.NewV7()),
|
||||
OrderID: orderID,
|
||||
ProductID: productIDs[0],
|
||||
ProductID: priceItem,
|
||||
Quantity: qty,
|
||||
UnitCents: price,
|
||||
Batch: "BATCH-" + productIDs[0].String()[:8],
|
||||
UnitCents: unitPrice,
|
||||
Batch: "BATCH-NAC-" + priceItem.String()[:4],
|
||||
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) {
|
||||
|
|
@ -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()
|
||||
settings := &domain.ShippingSettings{
|
||||
VendorID: vendorID,
|
||||
|
|
@ -231,12 +336,12 @@ func createShippingSettings(ctx context.Context, db *sqlx.DB, vendorID uuid.UUID
|
|||
MinFeeCents: 1000, // R$ 10.00
|
||||
FreeShippingThresholdCents: nil,
|
||||
PickupActive: true,
|
||||
PickupAddress: "Rua da Distribuidora, 1000",
|
||||
PickupAddress: address,
|
||||
PickupHours: "Seg-Sex 08-18h",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Latitude: -23.55052,
|
||||
Longitude: -46.633308,
|
||||
Latitude: lat,
|
||||
Longitude: lng,
|
||||
}
|
||||
|
||||
_, err := db.NamedExecContext(ctx, `
|
||||
|
|
|
|||
|
|
@ -4,17 +4,18 @@ import { useAuth, UserRole } from '../context/AuthContext'
|
|||
import { authService } from '../services/auth'
|
||||
import { logger } from '../utils/logger'
|
||||
import { decodeJwtPayload } from '../utils/jwt'
|
||||
import logoImg from '../assets/logo.png' // Ensure logo import is handled
|
||||
|
||||
// Eye icon components for password visibility toggle
|
||||
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="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
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" />
|
||||
</svg>
|
||||
)
|
||||
|
|
@ -26,25 +27,17 @@ export function LoginPage() {
|
|||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState<'login' | 'register'>('login')
|
||||
|
||||
const resolveRole = (role?: string): UserRole => {
|
||||
logger.info('🔐 [Login] Resolving role:', role)
|
||||
switch (role?.toLowerCase()) {
|
||||
case 'admin':
|
||||
return 'admin'
|
||||
case 'dono':
|
||||
return 'owner'
|
||||
case 'colaborador':
|
||||
return 'employee'
|
||||
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'
|
||||
case 'admin': return 'admin'
|
||||
case 'dono': return 'owner'
|
||||
case 'colaborador': return 'employee'
|
||||
case 'entregador': return 'delivery'
|
||||
case 'customer': return 'customer'
|
||||
case 'seller': default: return 'seller'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -53,37 +46,20 @@ export function LoginPage() {
|
|||
setLoading(true)
|
||||
setErrorMessage(null)
|
||||
|
||||
logger.info('🔐 [Login] Attempting login with username:', username)
|
||||
|
||||
try {
|
||||
logger.info('🔐 [Login] Calling authService.login...')
|
||||
const response = await authService.login({ username, password })
|
||||
logger.info('🔐 [Login] Response received:', response)
|
||||
|
||||
const { token } = response
|
||||
logger.debug('🔐 [Login] Token extracted:', token ? `${token.substring(0, 50)}...` : 'NULL/UNDEFINED')
|
||||
|
||||
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.')
|
||||
}
|
||||
if (!token) 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)
|
||||
logger.debug('🔐 [Login] JWT payload decoded:', payload)
|
||||
|
||||
const role = resolveRole(payload?.role)
|
||||
logger.info('🔐 [Login] Role resolved:', role)
|
||||
|
||||
login(token, role, username, payload?.sub || '', payload?.company_id, undefined, username)
|
||||
logger.info('🔐 [Login] Login successful!')
|
||||
} catch (error) {
|
||||
logger.error('🔐 [Login] ERROR caught:', error)
|
||||
const fallback = 'Não foi possível autenticar. Verifique suas credenciais.'
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error('🔐 [Login] Axios error response:', error.response?.data)
|
||||
setErrorMessage(error.response?.data?.error ?? fallback)
|
||||
} else if (error instanceof Error) {
|
||||
logger.error('🔐 [Login] Error message:', error.message)
|
||||
setErrorMessage(error.message)
|
||||
} else {
|
||||
setErrorMessage(fallback)
|
||||
|
|
@ -94,63 +70,124 @@ export function LoginPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gray-100">
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
className="w-full max-w-md space-y-4 rounded-lg bg-white p-8 shadow-lg"
|
||||
>
|
||||
<h1 className="text-2xl font-bold text-medicalBlue">Acesso ao Marketplace</h1>
|
||||
<p className="text-sm text-gray-600">Informe suas credenciais para acessar o marketplace.</p>
|
||||
{errorMessage && (
|
||||
<div className="rounded border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
|
||||
{errorMessage}
|
||||
<div className="flex min-h-screen items-center justify-center bg-gray-100 p-4">
|
||||
<div className="w-full max-w-[400px] overflow-hidden rounded-2xl bg-white shadow-xl">
|
||||
{/* Blue Header with Logo */}
|
||||
<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>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700" htmlFor="username">
|
||||
Usuário
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
autoComplete="username"
|
||||
className="w-full rounded border border-gray-200 px-3 py-2"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<h1 className="text-2xl font-bold">SaveInMed</h1>
|
||||
<p className="text-blue-100 text-sm">Plataforma B2B de Medicamentos</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700" htmlFor="password">
|
||||
Senha
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
autoComplete="current-password"
|
||||
className="w-full rounded border border-gray-200 px-3 py-2 pr-10"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
{/* 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'
|
||||
}`}
|
||||
>
|
||||
Entrar
|
||||
</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 && (
|
||||
<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">
|
||||
<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 className="space-y-1.5">
|
||||
<label className="text-sm font-medium text-gray-700 ml-1">Email ou Usuário</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
|
||||
id="username"
|
||||
name="username"
|
||||
autoComplete="username"
|
||||
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}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium text-gray-700 ml-1">Senha</label>
|
||||
<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
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
autoComplete="current-password"
|
||||
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}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
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)}
|
||||
>
|
||||
{showPassword ? <EyeSlashIcon /> : <EyeIcon />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
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}
|
||||
>
|
||||
{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>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="p-8 text-center text-gray-500 py-12">
|
||||
<p>Funcionalidade de cadastro em breve.</p>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700 focus:outline-none"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
aria-label={showPassword ? "Ocultar senha" : "Mostrar senha"}
|
||||
onClick={() => setActiveTab('login')}
|
||||
className="mt-4 text-blue-600 hover:underline text-sm"
|
||||
>
|
||||
{showPassword ? <EyeSlashIcon /> : <EyeIcon />}
|
||||
Voltar para login
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full rounded bg-healthGreen px-4 py-2 font-semibold text-white disabled:cursor-not-allowed disabled:opacity-70"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Entrando...' : 'Entrar'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,150 @@
|
|||
import { useState } from 'react'
|
||||
import { Shell } from '../layouts/Shell'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { adminService } from '../services/adminService'
|
||||
|
||||
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 (
|
||||
<Shell>
|
||||
<div className="space-y-6 rounded-lg bg-white p-6 shadow-sm">
|
||||
<div>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="rounded-md border border-gray-200 p-4">
|
||||
<p className="text-xs uppercase text-gray-400">Nome</p>
|
||||
<p className="text-sm font-semibold text-gray-800">{user?.name ?? 'Não informado'}</p>
|
||||
|
||||
<form onSubmit={handleSave} className="grid gap-6">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="rounded-md border border-gray-200 p-4 relative">
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{isEditing && <span className="text-[10px] text-gray-500 absolute top-2 right-2">(Não editável)</span>}
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-gray-200 p-4">
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-gray-200 p-4">
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-md border border-gray-200 p-4">
|
||||
<p className="text-xs uppercase text-gray-400">Perfil</p>
|
||||
<p className="text-sm font-semibold text-gray-800">{user?.role ?? 'Não informado'}</p>
|
||||
</div>
|
||||
<div className="rounded-md border border-gray-200 p-4">
|
||||
<p className="text-xs uppercase text-gray-400">E-mail</p>
|
||||
<p className="text-sm font-semibold text-gray-800">{user?.email ?? 'Não informado'}</p>
|
||||
</div>
|
||||
<div className="rounded-md border border-gray-200 p-4">
|
||||
<p className="text-xs uppercase text-gray-400">Usuário</p>
|
||||
<p className="text-sm font-semibold text-gray-800">{user?.username ?? 'Não informado'}</p>
|
||||
</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>
|
||||
</Shell>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -119,6 +119,11 @@ const ProductSearch = () => {
|
|||
}
|
||||
}, [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) => {
|
||||
setLat(newLat)
|
||||
setLng(newLng)
|
||||
|
|
@ -261,7 +266,7 @@ const ProductSearch = () => {
|
|||
<div className="lg:col-span-3">
|
||||
<div className="mb-4 flex justify-between items-center">
|
||||
<h2 className="text-xl font-semibold text-gray-700">
|
||||
{loading ? 'Buscando...' : `${groupedProducts.length} medicamentos (${total} ofertas)`}
|
||||
{loading ? 'Buscando...' : `${groupedProducts.length} medicamentos (${visibleOffers} ofertas)`}
|
||||
</h2>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-gray-500">Ordenar por:</span>
|
||||
|
|
|
|||
Loading…
Reference in a new issue