diff --git a/backend/internal/domain/models.go b/backend/internal/domain/models.go index f5444e9..b4b515e 100644 --- a/backend/internal/domain/models.go +++ b/backend/internal/domain/models.go @@ -26,6 +26,14 @@ type Tenant struct { // Company is an alias for Tenant for backward compatibility. type Company = Tenant +// Role constants +const ( + RoleAdmin = "Admin" + RoleOwner = "Dono" + RoleEmployee = "Colaborador" + RoleDelivery = "Entregador" +) + // User represents an authenticated actor inside a company. type User struct { ID uuid.UUID `db:"id" json:"id"` diff --git a/marketplace/src/App.tsx b/marketplace/src/App.tsx index beb4394..723cec0 100644 --- a/marketplace/src/App.tsx +++ b/marketplace/src/App.tsx @@ -8,20 +8,56 @@ import { OrdersPage } from './pages/Orders' import { InventoryPage } from './pages/Inventory' import { CompanyPage } from './pages/Company' import { SellerDashboardPage } from './pages/SellerDashboard' +import { AdminDashboardPage } from './pages/AdminDashboard' +import { EmployeeDashboardPage } from './pages/EmployeeDashboard' +import { DeliveryDashboardPage } from './pages/DeliveryDashboard' import { ProtectedRoute } from './components/ProtectedRoute' function App() { return ( } /> + + {/* Owner / Seller Dashboard */} + } /> + + {/* Admin Dashboard */} + + + + } + /> + + {/* Employee (Colaborador) Dashboard */} + + + + } + /> + + {/* Delivery (Entregador) Dashboard */} + + + + } + /> + } /> - } /> + } /> ) } diff --git a/marketplace/src/context/AuthContext.tsx b/marketplace/src/context/AuthContext.tsx index 823eb76..420572b 100644 --- a/marketplace/src/context/AuthContext.tsx +++ b/marketplace/src/context/AuthContext.tsx @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom' import { apiClient } from '../services/apiClient' import { authService } from '../services/auth' -export type UserRole = 'admin' | 'seller' | 'customer' +export type UserRole = 'admin' | 'owner' | 'employee' | 'delivery' | 'seller' | 'customer' export interface AuthUser { name: string @@ -46,7 +46,25 @@ export function AuthProvider({ children }: { children: ReactNode }) { const login = (token: string, role: UserRole, name: string) => { setUser({ token, role, name }) - navigate('/dashboard', { replace: true }) + + // Redirect based on role + switch (role) { + case 'admin': + navigate('/admin', { replace: true }) + break + case 'owner': + case 'seller': + navigate('/dashboard', { replace: true }) + break + case 'employee': + navigate('/colaborador', { replace: true }) + break + case 'delivery': + navigate('/entregas', { replace: true }) + break + default: + navigate('/dashboard', { replace: true }) + } } const logout = () => { diff --git a/marketplace/src/pages/AdminDashboard.tsx b/marketplace/src/pages/AdminDashboard.tsx new file mode 100644 index 0000000..e06a49c --- /dev/null +++ b/marketplace/src/pages/AdminDashboard.tsx @@ -0,0 +1,32 @@ +import { useAuth } from '../context/AuthContext' + +export function AdminDashboardPage() { + const { user, logout } = useAuth() + + return ( +
+
+
+
+

Painel do Administrador

+

Bem-vindo, {user?.name} (Admin)

+
+ +
+ +
+
+

Resumo Geral

+

Visão geral do sistema.

+
+ {/* Add more admin widgets here */} +
+
+
+ ) +} diff --git a/marketplace/src/pages/DeliveryDashboard.tsx b/marketplace/src/pages/DeliveryDashboard.tsx new file mode 100644 index 0000000..78c16a1 --- /dev/null +++ b/marketplace/src/pages/DeliveryDashboard.tsx @@ -0,0 +1,30 @@ +import { useAuth } from '../context/AuthContext' + +export function DeliveryDashboardPage() { + const { user, logout } = useAuth() + + return ( +
+
+
+
+

Painel do Entregador

+

Bem-vindo, {user?.name}

+
+ +
+ +
+

Minhas Entregas

+

Visualize as entregas pendentes e o mapa de rotas.

+ {/* Map Integration would go here */} +
+
+
+ ) +} diff --git a/marketplace/src/pages/EmployeeDashboard.tsx b/marketplace/src/pages/EmployeeDashboard.tsx new file mode 100644 index 0000000..5822e7d --- /dev/null +++ b/marketplace/src/pages/EmployeeDashboard.tsx @@ -0,0 +1,37 @@ +import { useAuth } from '../context/AuthContext' + +export function EmployeeDashboardPage() { + const { user, logout } = useAuth() + + return ( +
+
+
+
+

Painel do Colaborador

+

Bem-vindo, {user?.name}

+
+ +
+ +
+
+

Pedidos

+

Gerenciar pedidos recebidos.

+ {/* Link to Orders */} +
+
+

Estoque

+

Consultar e ajustar estoque.

+ {/* Link to Inventory */} +
+
+
+
+ ) +} diff --git a/marketplace/src/pages/Login.tsx b/marketplace/src/pages/Login.tsx index efb2e8f..8ae7fb7 100644 --- a/marketplace/src/pages/Login.tsx +++ b/marketplace/src/pages/Login.tsx @@ -12,14 +12,22 @@ export function LoginPage() { const [loading, setLoading] = useState(false) const resolveRole = (role?: string): UserRole => { - console.log('🔐 [Login] Component rendering') + console.log('🔐 [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': + case 'seller': // keep legacy default: + // Default to seller/owner or log warning? + console.warn('⚠️ [Login] Unknown role, defaulting to seller:', role) return 'seller' } } diff --git a/seeder-api/main.go b/seeder-api/main.go index ed5009b..666c394 100644 --- a/seeder-api/main.go +++ b/seeder-api/main.go @@ -28,7 +28,8 @@ func main() { return } - result, err := seeder.Seed(dsn) + mode := r.URL.Query().Get("mode") + result, err := seeder.Seed(dsn, mode) if err != nil { log.Printf("Seeder error: %v", err) http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/seeder-api/pkg/seeder/seeder.go b/seeder-api/pkg/seeder/seeder.go index 55ca000..2fe9a92 100644 --- a/seeder-api/pkg/seeder/seeder.go +++ b/seeder-api/pkg/seeder/seeder.go @@ -11,6 +11,7 @@ import ( "github.com/gofrs/uuid/v5" _ "github.com/jackc/pgx/v5/stdlib" "github.com/jmoiron/sqlx" + "golang.org/x/crypto/bcrypt" ) // Anápolis, GO coordinates @@ -66,7 +67,183 @@ var pharmacyNames = []string{ "Vida Saudável", "Mais Saúde", "Farmácia do Povo", "Super Farma", } -func Seed(dsn string) (string, error) { +// Seed dispatches based on mode +func Seed(dsn, mode string) (string, error) { + if mode == "lean" { + return SeedLean(dsn) + } + return SeedFull(dsn) +} + +func SeedLean(dsn string) (string, error) { + if dsn == "" { + return "", fmt.Errorf("DATABASE_URL not set") + } + + db, err := sqlx.Connect("pgx", dsn) + if err != nil { + return "", fmt.Errorf("db connect: %v", err) + } + defer db.Close() + + ctx := context.Background() + log.Println("🧹 [Lean] Resetting database...") + + // Re-create tables + mustExec(db, `DROP TABLE IF EXISTS inventory_adjustments CASCADE`) + mustExec(db, `DROP TABLE IF EXISTS order_items CASCADE`) + mustExec(db, `DROP TABLE IF EXISTS orders CASCADE`) + mustExec(db, `DROP TABLE IF EXISTS cart_items CASCADE`) + mustExec(db, `DROP TABLE IF EXISTS reviews CASCADE`) + mustExec(db, `DROP TABLE IF EXISTS products CASCADE`) + mustExec(db, `DROP TABLE IF EXISTS users CASCADE`) + mustExec(db, `DROP TABLE IF EXISTS companies CASCADE`) + + // Create tables (Schema must match backend migrations!) + mustExec(db, `CREATE TABLE companies ( + id UUID PRIMARY KEY, + cnpj TEXT NOT NULL UNIQUE, + corporate_name TEXT NOT NULL, + category TEXT NOT NULL DEFAULT 'farmacia', + license_number TEXT NOT NULL, + is_verified BOOLEAN NOT NULL DEFAULT FALSE, + latitude DOUBLE PRECISION NOT NULL DEFAULT 0, + longitude DOUBLE PRECISION NOT NULL DEFAULT 0, + city TEXT NOT NULL DEFAULT '', + state TEXT NOT NULL DEFAULT '', + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL + )`) + + // Add missing users table creation here to be complete for independent seeder run + mustExec(db, `CREATE TABLE users ( + id UUID PRIMARY KEY, + company_id UUID NOT NULL REFERENCES companies(id), + role TEXT NOT NULL, + name TEXT NOT NULL, + username TEXT NOT NULL UNIQUE, + email TEXT NOT NULL UNIQUE, + email_verified BOOLEAN NOT NULL DEFAULT FALSE, + password_hash TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL + )`) + + mustExec(db, `CREATE TABLE products ( + id UUID PRIMARY KEY, + seller_id UUID NOT NULL REFERENCES companies(id), + name TEXT NOT NULL, + description TEXT, + batch TEXT NOT NULL, + expires_at DATE NOT NULL, + price_cents BIGINT NOT NULL, + stock BIGINT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL + )`) + + // Create 1 Pharmacy + pharmacyID := uuid.Must(uuid.NewV7()) + now := time.Now().UTC() + + _, err = db.ExecContext(ctx, ` + INSERT INTO companies (id, cnpj, corporate_name, category, license_number, is_verified, latitude, longitude, city, state, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`, + pharmacyID, "12345678000199", "Farmácia Modelo", "farmacia", "CRF-GO-12345", true, AnapolisLat, AnapolisLng, "Anápolis", "GO", now, now, + ) + if err != nil { + return "", fmt.Errorf("create pharmacy: %v", err) + } + + // Create standard password hash (e.g. "123456") + // Using a fixed hash for speed/reproducibility. hash("$2a$10$3Y... for '123456'") + // Or generating one? Let's use a known hash from backend or generate one locally if possible. + // To avoid dep on bcrypt, I will assume one. + // But `users` table needs it. + // "123456" bcrypt hash (cost 10): $2a$10$Vj.uOq/e/3.t/2.r/1.s/e + // "admin123" bcrypt hash: $2y$10$vI8aWBdWs/.r/2/.r.. (Let's stick to "123456" for simplicity or use one from backend?) + // User requested "dono/123456". + pwdHash123456 := "$2a$10$x86K.S/3/1./2./3./4./5./6./" // PLACHOLDER? No, I should generate or use a real one. + // Real hash for "123456" generated previously or online: $2a$10$2.1.1.1.1.1.1.1.1.1.1. + // Actually, I'll use a mocked valid hash. + // $2a$10$2.1.1.1.1.1.1.1.1.1.1 is not valid. + // I'll leave a TODO or use a hardcoded one if I can. + // Better: use the same one as Admin ("admin123" -> "$2a$10$...") + + // Let's use a valid hash for '123456'. + // Generated: $2a$10$4.1.1.1.1.1.1.1.1.1.1. (Fake) + // I will use a known one. From previous logs? + // In `server.go`, admin password is env var. + // I'll grab a valid hash for "123456" -> `$2a$10$6.1.1.1.1.1.1.1.1.1.1` (Just kidding). + // I'll use a placeholder that works. + validHash123456 := "$2a$10$e.g.e.g.e.g.e.g.e.g.e.g." // Requires real generation. + // I'll import bcrypt? + // `seeder-api` doesn't have bcrypt in imports? + // It has `math/rand`, `time`. + // I should add `golang.org/x/crypto/bcrypt` if needed or use raw SQL pgcrypto if available. + // I'll add bcrypt to imports in a separate step or just assume the hash. + // Let's assume the hash for "123456" is: $2a$10$N.z.y.x... + + // I'll proceed with creating users: + + // Helper for hashing + hashPwd := func(pwd string) string { + h, _ := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost) + return string(h) + } + + // 1. Admin + adminID := uuid.Must(uuid.NewV7()) + mustExec(db, fmt.Sprintf(`INSERT INTO users (id, company_id, role, name, username, email, email_verified, password_hash, created_at, updated_at) + VALUES ('%s', '%s', 'Admin', 'Administrador', 'admin', 'admin@saveinmed.com', true, '%s', NOW(), NOW())`, + adminID, pharmacyID, hashPwd("admin123"), + )) + + // 2. Owner (Dono) + ownerID := uuid.Must(uuid.NewV7()) + mustExec(db, fmt.Sprintf(`INSERT INTO users (id, company_id, role, name, username, email, email_verified, password_hash, created_at, updated_at) + VALUES ('%s', '%s', 'Dono', 'João Dono', 'dono', 'dono@farmacia.com', true, '%s', NOW(), NOW())`, + ownerID, pharmacyID, hashPwd("123456"), + )) + + // 3. Employee (Colaborador) + empID := uuid.Must(uuid.NewV7()) + mustExec(db, fmt.Sprintf(`INSERT INTO users (id, company_id, role, name, username, email, email_verified, password_hash, created_at, updated_at) + VALUES ('%s', '%s', 'Colaborador', 'Maria Colaboradora', 'colaborador', 'colaborador@farmacia.com', true, '%s', NOW(), NOW())`, + empID, pharmacyID, hashPwd("123456"), + )) + + // 4. Delivery (Entregador) + // Delivery person usually needs their own "company" or is linked to the pharmacy? + // For now, linking to the same pharmacy for simplicity, or creating a carrier? + // The prompt implies "entregador" as a user role. + // Linking to Pharmacy for simplicity (internal delivery fleet). + delID := uuid.Must(uuid.NewV7()) + mustExec(db, fmt.Sprintf(`INSERT INTO users (id, company_id, role, name, username, email, email_verified, password_hash, created_at, updated_at) + VALUES ('%s', '%s', 'Entregador', 'José Entregador', 'entregador', 'entregador@farmacia.com', true, '%s', NOW(), NOW())`, + delID, pharmacyID, hashPwd("123456"), + )) + + log.Println("✅ [Lean] Users created: admin, dono, colaborador, entregador") + + // Create Products for the Pharmacy + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + products := generateProducts(rng, pharmacyID, 15) + for _, p := range products { + _, err := db.NamedExecContext(ctx, ` + INSERT INTO products (id, seller_id, name, description, batch, expires_at, price_cents, stock, created_at, updated_at) + VALUES (:id, :seller_id, :name, :description, :batch, :expires_at, :price_cents, :stock, :created_at, :updated_at) + ON CONFLICT DO NOTHING`, p) + if err != nil { + log.Printf("insert product lean: %v", err) + } + } + log.Println("✅ [Lean] Created 15 products") + + return fmt.Sprintf("Lean seed completed. Users: admin, dono, colaborador, entregador (Pass: 123456/admin123)"), nil +} + +func SeedFull(dsn string) (string, error) { if dsn == "" { return "", fmt.Errorf("DATABASE_URL not set") } @@ -106,6 +283,19 @@ func Seed(dsn string) (string, error) { updated_at TIMESTAMPTZ NOT NULL )`) + mustExec(db, `CREATE TABLE users ( + id UUID PRIMARY KEY, + company_id UUID NOT NULL REFERENCES companies(id), + role TEXT NOT NULL, + name TEXT NOT NULL, + username TEXT NOT NULL UNIQUE, + email TEXT NOT NULL UNIQUE, + email_verified BOOLEAN NOT NULL DEFAULT FALSE, + password_hash TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL + )`) + mustExec(db, `CREATE TABLE products ( id UUID PRIMARY KEY, seller_id UUID NOT NULL REFERENCES companies(id),