diff --git a/backend/cmd/seeder/main.go b/backend/cmd/seeder/main.go index ec6f0e2..7481103 100644 --- a/backend/cmd/seeder/main.go +++ b/backend/cmd/seeder/main.go @@ -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, ` diff --git a/marketplace/src/pages/Login.tsx b/marketplace/src/pages/Login.tsx index 47e2768..027a0d5 100644 --- a/marketplace/src/pages/Login.tsx +++ b/marketplace/src/pages/Login.tsx @@ -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 = () => ( - + ) const EyeSlashIcon = () => ( - + ) @@ -26,25 +27,17 @@ export function LoginPage() { const [showPassword, setShowPassword] = useState(false) const [errorMessage, setErrorMessage] = useState(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 ( -
-
-

Acesso ao Marketplace

-

Informe suas credenciais para acessar o marketplace.

- {errorMessage && ( -
- {errorMessage} +
+
+ {/* Blue Header with Logo */} +
+
+ Logo
- )} -
- - setUsername(e.target.value)} - /> +

SaveInMed

+

Plataforma B2B de Medicamentos

-
- -
- setPassword(e.target.value)} - /> + + {/* Tabs */} +
+ + +
+ + {/* Login Form */} + {activeTab === 'login' ? ( + + {errorMessage && ( +
+ + + + credenciais inválidas, tente novamente +
+ )} + +
+ +
+
+ @ +
+ setUsername(e.target.value)} + /> +
+
+ +
+ +
+
+ +
+ setPassword(e.target.value)} + /> + +
+
+ +
+ +
+ + ) : ( +
+

Funcionalidade de cadastro em breve.

-
- - + )} + +
) } diff --git a/marketplace/src/pages/MyProfile.tsx b/marketplace/src/pages/MyProfile.tsx index 7fb4130..57e0288 100644 --- a/marketplace/src/pages/MyProfile.tsx +++ b/marketplace/src/pages/MyProfile.tsx @@ -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) => { + 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 ( -
-
-

Meu Perfil

-

Acompanhe as informações básicas da sua conta.

+
+
+
+

Meu Perfil

+

Acompanhe as informações básicas da sua conta.

+
+ {!isEditing ? ( + + ) : ( + + )}
-
-
-

Nome

-

{user?.name ?? 'Não informado'}

+ +
+
+
+ + {isEditing ? ( + + ) : ( +

{user?.name ?? 'Não informado'}

+ )} +
+ +
+

Perfil

+

{user?.role ?? 'Não informado'}

+ {isEditing && (Não editável)} +
+ +
+ + {isEditing ? ( + + ) : ( +

{user?.email ?? 'Não informado'}

+ )} +
+ +
+ + {isEditing ? ( + + ) : ( +

{user?.username ?? 'Não informado'}

+ )} +
-
-

Perfil

-

{user?.role ?? 'Não informado'}

-
-
-

E-mail

-

{user?.email ?? 'Não informado'}

-
-
-

Usuário

-

{user?.username ?? 'Não informado'}

-
-
+ + {isEditing && ( +
+ +
+ )} +
) diff --git a/marketplace/src/pages/ProductSearch.tsx b/marketplace/src/pages/ProductSearch.tsx index c8970da..54a03e5 100644 --- a/marketplace/src/pages/ProductSearch.tsx +++ b/marketplace/src/pages/ProductSearch.tsx @@ -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 = () => {

- {loading ? 'Buscando...' : `${groupedProducts.length} medicamentos (${total} ofertas)`} + {loading ? 'Buscando...' : `${groupedProducts.length} medicamentos (${visibleOffers} ofertas)`}

Ordenar por: