diff --git a/backend/go.mod b/backend/go.mod index 64828eb..0df4863 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -9,7 +9,6 @@ require ( github.com/jackc/pgx/v5 v5.7.6 github.com/jmoiron/sqlx v1.4.0 github.com/joho/godotenv v1.5.1 - github.com/json-iterator/go v1.1.12 github.com/lib/pq v1.10.9 github.com/stretchr/testify v1.8.1 github.com/swaggo/http-swagger v1.3.4 @@ -29,8 +28,6 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.6 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/stretchr/objx v0.5.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index 772e8ec..2bdbbe8 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -24,7 +24,6 @@ github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0= github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -39,8 +38,6 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= @@ -57,10 +54,6 @@ github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/frontend/package.json b/frontend/package.json index fc307b9..5dde9b6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "lucide-react": "^0.562.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-icons": "^5.5.0", "react-leaflet": "^4.2.1", "react-router-dom": "^6.26.0", "react-window": "^1.8.10", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 6a69982..45bd083 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-icons: + specifier: ^5.5.0 + version: 5.5.0(react@18.3.1) react-leaflet: specifier: ^4.2.1 version: 4.2.1(leaflet@1.9.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -51,6 +54,9 @@ importers: '@testing-library/user-event': specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.1) + '@types/node': + specifier: ^22.0.0 + version: 22.19.12 '@types/react': specifier: ^18.3.7 version: 18.3.27 @@ -62,10 +68,10 @@ importers: version: 1.8.8 '@vitejs/plugin-react': specifier: ^4.7.0 - version: 4.7.0(vite@5.4.21) + version: 4.7.0(vite@5.4.21(@types/node@22.19.12)) '@vitest/coverage-v8': specifier: ^4.0.16 - version: 4.0.16(vitest@4.0.16(jiti@1.21.7)(jsdom@27.4.0)) + version: 4.0.16(vitest@4.0.16(@types/node@22.19.12)(jiti@1.21.7)(jsdom@27.4.0)) autoprefixer: specifier: ^10.4.20 version: 10.4.23(postcss@8.5.6) @@ -83,10 +89,10 @@ importers: version: 5.9.3 vite: specifier: ^5.4.3 - version: 5.4.21 + version: 5.4.21(@types/node@22.19.12) vitest: specifier: ^4.0.16 - version: 4.0.16(jiti@1.21.7)(jsdom@27.4.0) + version: 4.0.16(@types/node@22.19.12)(jiti@1.21.7)(jsdom@27.4.0) packages: @@ -620,56 +626,67 @@ packages: resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.54.0': resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.54.0': resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.54.0': resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.54.0': resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.54.0': resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.54.0': resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.54.0': resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.54.0': resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.54.0': resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.54.0': resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openharmony-arm64@4.54.0': resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==} @@ -758,6 +775,9 @@ packages: '@types/leaflet@1.9.21': resolution: {integrity: sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==} + '@types/node@22.19.12': + resolution: {integrity: sha512-0QEp0aPJYSyf6RrTjDB7HlKgNMTY+V2C7ESTaVt6G9gQ0rPLzTGz7OF2NXTLR5vcy7HJEtIUsyWLsfX0kTqJBA==} + '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} @@ -1385,6 +1405,11 @@ packages: peerDependencies: react: ^18.3.1 + react-icons@5.5.0: + resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==} + peerDependencies: + react: '*' + react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -1558,6 +1583,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -2237,6 +2265,10 @@ snapshots: dependencies: '@types/geojson': 7946.0.16 + '@types/node@22.19.12': + dependencies: + undici-types: 6.21.0 + '@types/prop-types@15.7.15': {} '@types/react-dom@18.3.7(@types/react@18.3.27)': @@ -2252,7 +2284,7 @@ snapshots: '@types/prop-types': 15.7.15 csstype: 3.2.3 - '@vitejs/plugin-react@4.7.0(vite@5.4.21)': + '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.19.12))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) @@ -2260,11 +2292,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 5.4.21 + vite: 5.4.21(@types/node@22.19.12) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@4.0.16(vitest@4.0.16(jiti@1.21.7)(jsdom@27.4.0))': + '@vitest/coverage-v8@4.0.16(vitest@4.0.16(@types/node@22.19.12)(jiti@1.21.7)(jsdom@27.4.0))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.16 @@ -2277,7 +2309,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.16(jiti@1.21.7)(jsdom@27.4.0) + vitest: 4.0.16(@types/node@22.19.12)(jiti@1.21.7)(jsdom@27.4.0) transitivePeerDependencies: - supports-color @@ -2290,13 +2322,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.16(vite@7.3.0(jiti@1.21.7))': + '@vitest/mocker@4.0.16(vite@7.3.0(@types/node@22.19.12)(jiti@1.21.7))': dependencies: '@vitest/spy': 4.0.16 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.0(jiti@1.21.7) + vite: 7.3.0(@types/node@22.19.12)(jiti@1.21.7) '@vitest/pretty-format@4.0.16': dependencies: @@ -2879,6 +2911,10 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-icons@5.5.0(react@18.3.1): + dependencies: + react: 18.3.1 + react-is@17.0.2: {} react-leaflet@4.2.1(leaflet@1.9.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -3079,6 +3115,8 @@ snapshots: typescript@5.9.3: {} + undici-types@6.21.0: {} + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 @@ -3091,15 +3129,16 @@ snapshots: util-deprecate@1.0.2: {} - vite@5.4.21: + vite@5.4.21(@types/node@22.19.12): dependencies: esbuild: 0.21.5 postcss: 8.5.6 rollup: 4.54.0 optionalDependencies: + '@types/node': 22.19.12 fsevents: 2.3.3 - vite@7.3.0(jiti@1.21.7): + vite@7.3.0(@types/node@22.19.12)(jiti@1.21.7): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -3108,13 +3147,14 @@ snapshots: rollup: 4.54.0 tinyglobby: 0.2.15 optionalDependencies: + '@types/node': 22.19.12 fsevents: 2.3.3 jiti: 1.21.7 - vitest@4.0.16(jiti@1.21.7)(jsdom@27.4.0): + vitest@4.0.16(@types/node@22.19.12)(jiti@1.21.7)(jsdom@27.4.0): dependencies: '@vitest/expect': 4.0.16 - '@vitest/mocker': 4.0.16(vite@7.3.0(jiti@1.21.7)) + '@vitest/mocker': 4.0.16(vite@7.3.0(@types/node@22.19.12)(jiti@1.21.7)) '@vitest/pretty-format': 4.0.16 '@vitest/runner': 4.0.16 '@vitest/snapshot': 4.0.16 @@ -3131,9 +3171,10 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.0(jiti@1.21.7) + vite: 7.3.0(@types/node@22.19.12)(jiti@1.21.7) why-is-node-running: 2.3.0 optionalDependencies: + '@types/node': 22.19.12 jsdom: 27.4.0 transitivePeerDependencies: - jiti diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fa8ef25..c970874 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,6 +2,8 @@ import { Navigate, Route, Routes } from 'react-router-dom' // Auth import { LoginPage } from '@/pages/auth/Login' +import { ForgotPasswordPage } from '@/pages/auth/ForgotPassword' +import { RegisterPage } from '@/pages/auth/Register' // Marketplace import { CartPage } from '@/pages/marketplace/Cart' @@ -46,6 +48,8 @@ function App() { return ( } /> + } /> + } /> {/* Admin Dashboard with Header Layout */} + } @@ -110,7 +114,7 @@ function App() { + } @@ -118,7 +122,7 @@ function App() { + } @@ -174,7 +178,7 @@ function App() { + } diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index eb45926..70197a9 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -1,8 +1,9 @@ import { useEffect, useRef, useState } from 'react' import { Link, useLocation } from 'react-router-dom' import { useAuth } from '../context/AuthContext' +import logoImg from '../assets/logo.png' -const navItems = [ +const adminNavItems = [ { path: '/dashboard', label: 'Início' }, { path: '/dashboard/users', label: 'Usuários' }, { path: '/dashboard/companies', label: 'Empresas' }, @@ -18,6 +19,10 @@ export function Header() { const [isOpen, setIsOpen] = useState(false) const dropdownRef = useRef(null) + // Só mostrar nav para admin. Entregador, seller, employee não precisam dos itens de admin. + const isAdmin = user?.role === 'admin' + const navItems = isAdmin ? adminNavItems : [] + useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { @@ -32,36 +37,36 @@ export function Header() { }, []) return ( -
+
{/* Logo */} -
- 💊 -
+ SaveInMed SaveInMed - {/* Navigation */} - + {/* Navigation — only for admin */} + {navItems.length > 0 && ( + + )} {/* User info */}
@@ -87,7 +92,7 @@ export function Header() { {isOpen && ( -
+
- {/* Mobile Navigation */} - + {/* Mobile Navigation — only for admin */} + {navItems.length > 0 && ( + + )}
) } diff --git a/frontend/src/components/LocationPicker.tsx b/frontend/src/components/LocationPicker.tsx index f6913b6..2ddf69b 100644 --- a/frontend/src/components/LocationPicker.tsx +++ b/frontend/src/components/LocationPicker.tsx @@ -1,5 +1,5 @@ import { MapContainer, TileLayer, Marker, useMapEvents } from 'react-leaflet' -import { useState, useEffect } from 'react' +import { useState } from 'react' import L from 'leaflet' import 'leaflet/dist/leaflet.css' @@ -7,7 +7,7 @@ import 'leaflet/dist/leaflet.css' import icon from 'leaflet/dist/images/marker-icon.png' import iconShadow from 'leaflet/dist/images/marker-shadow.png' -let DefaultIcon = L.icon({ +const DefaultIcon = L.icon({ iconUrl: icon, shadowUrl: iconShadow, iconSize: [25, 41], @@ -16,6 +16,12 @@ let DefaultIcon = L.icon({ L.Marker.prototype.options.icon = DefaultIcon +// Use env vars with safe fallback to OpenStreetMap +const TILE_URL = import.meta.env.VITE_MAP_TILE_LAYER || + 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' +const TILE_ATTR = import.meta.env.VITE_MAP_ATTRIBUTION || + '© OpenStreetMap contributors' + interface LocationPickerProps { initialLat?: number initialLng?: number @@ -46,8 +52,8 @@ export const LocationPicker = ({ initialLat, initialLng, onLocationSelect }: Loc
{(initialLat && initialLng) && } diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx new file mode 100644 index 0000000..0c0f51e --- /dev/null +++ b/frontend/src/components/Sidebar.tsx @@ -0,0 +1,192 @@ +import { useState } from 'react' +import { Link, useLocation } from 'react-router-dom' +import { useAuth, UserRole } from '@/context/AuthContext' +import { Menu, X } from 'lucide-react' +import logoImg from '../assets/logo.png' + +interface SidebarItem { + label: string + path: string + icon: JSX.Element + roles: UserRole[] +} + +/** + * Mapa de permissões por role: + * + * admin → usa o Header horizontal, sem Sidebar + * owner → Meu Painel, Estoque, Produtos, Pedidos, Carteira, Equipe, Config. Entrega + * seller → Meu Painel, Estoque, Produtos, Pedidos, Carteira, Equipe, Config. Entrega + * employee → usa Header horizontal sem Sidebar (reescrito) + * delivery → usa Header horizontal sem Sidebar (reescrito) + * customer → Sidebar não é usado + */ +const menuItems: SidebarItem[] = [ + // ─── Owner / Seller ─────────────────────────────────── + { + label: 'Meu Painel', + path: '/seller', + icon: ( + + + + ), + roles: ['owner', 'seller'], + }, + { + label: 'Estoque', + path: '/inventory', + icon: ( + + + + ), + roles: ['owner', 'seller'], + }, + { + label: 'Buscar Produtos', + path: '/search', + icon: ( + + + + ), + roles: ['owner', 'seller'], + // employee NÃO vê Produtos no sidebar (ele tem o próprio dashboard) + }, + { + label: 'Pedidos', + path: '/orders', + icon: ( + + + + ), + roles: ['owner', 'seller'], + }, + { + label: 'Carteira', + path: '/wallet', + icon: ( + + + + ), + roles: ['owner', 'seller'], + }, + { + label: 'Equipe', + path: '/team', + icon: ( + + + + ), + roles: ['owner', 'seller'], + }, + { + label: 'Config. de Entrega', + path: '/shipping-settings', + icon: ( + + + + ), + roles: ['owner', 'seller'], + }, +] + +interface SidebarProps { + /** @deprecated — passe userRole via prop ou deixe o Sidebar ler do useAuth automaticamente */ + userRole?: UserRole + collapsed?: boolean + onNavigate?: () => void +} + +export function Sidebar({ userRole: userRoleProp, collapsed: initialCollapsed, onNavigate }: SidebarProps) { + const [collapsed, setCollapsed] = useState(initialCollapsed || false) + const location = useLocation() + const { user } = useAuth() + + // Prioriza a prop (legado), mas usa a role do contexto se não houver prop + const role: UserRole = userRoleProp ?? user?.role ?? 'seller' + + // Filtra itens com base na role real + const visibleItems = menuItems.filter(item => item.roles.includes(role)) + + // Se não há itens visíveis para esta role, não renderiza o Sidebar + if (visibleItems.length === 0) return null + + return ( + <> + {/* Mobile toggle button */} + + + {/* Sidebar */} + + + {/* Backdrop for mobile */} + {!collapsed && ( +
setCollapsed(true)} + /> + )} + + ) +} diff --git a/frontend/src/layouts/RoleBasedLayout.tsx b/frontend/src/layouts/RoleBasedLayout.tsx new file mode 100644 index 0000000..9c4e188 --- /dev/null +++ b/frontend/src/layouts/RoleBasedLayout.tsx @@ -0,0 +1,22 @@ +import { Outlet } from 'react-router-dom' +import { useAuth } from '@/context/AuthContext' +import { Sidebar } from '@/components/Sidebar' + +export function RoleBasedLayout() { + const { user } = useAuth() + + if (!user) { + return null + } + + return ( +
+ +
+
+ +
+
+
+ ) +} diff --git a/frontend/src/layouts/Shell.tsx b/frontend/src/layouts/Shell.tsx index 6a83599..baecb53 100644 --- a/frontend/src/layouts/Shell.tsx +++ b/frontend/src/layouts/Shell.tsx @@ -4,6 +4,7 @@ import { useAuth } from '../context/AuthContext' import { useCartStore, selectCartSummary } from '../stores/cartStore' import { formatCurrency } from '../utils/format' import logoImg from '../assets/logo.png' +import { FaRightFromBracket } from 'react-icons/fa6' // Cart dropdown content component function CartDropdownContent() { @@ -90,117 +91,131 @@ export function Shell({ children }: { children: React.ReactNode }) { return (
-
-
- SaveInMed -
-

SaveInMed

-

- {isAdmin ? 'Painel Administrativo' : isOwner ? 'Painel do Dono' : 'Marketplace B2B'} -

+
+
+
+
+ SaveInMed + SaveInMed +
+
-
-
{children}
+
{children}
) } diff --git a/frontend/src/pages/auth/ForgotPassword.tsx b/frontend/src/pages/auth/ForgotPassword.tsx new file mode 100644 index 0000000..b2282be --- /dev/null +++ b/frontend/src/pages/auth/ForgotPassword.tsx @@ -0,0 +1,156 @@ +import { FormEvent, useState } from 'react' +import { Link } from 'react-router-dom' +import axios from 'axios' +import { authService } from '@/services/auth' +import logoImg from '@/assets/logo.png' + +export function ForgotPasswordPage() { + const [email, setEmail] = useState('') + const [loading, setLoading] = useState(false) + const [successMessage, setSuccessMessage] = useState(null) + const [errorMessage, setErrorMessage] = useState(null) + const [submitted, setSubmitted] = useState(false) + + const onSubmit = async (event: FormEvent) => { + event.preventDefault() + setLoading(true) + setErrorMessage(null) + + try { + await authService.forgotPassword({ email }) + setSuccessMessage('Um link de recuperação foi enviado para seu e-mail. Verifique sua caixa de entrada.') + setSubmitted(true) + setEmail('') + } catch (error) { + const fallback = 'Não foi possível enviar o link de recuperação. Tente novamente.' + if (axios.isAxiosError(error)) { + setErrorMessage(error.response?.data?.error ?? fallback) + } else if (error instanceof Error) { + setErrorMessage(error.message) + } else { + setErrorMessage(fallback) + } + } finally { + setLoading(false) + } + } + + return ( +
+
+ {/* Blue Header with Logo */} +
+
+ Logo +
+

SaveInMed

+

Recuperar Acesso

+
+ + {/* Form Content */} +
+ {successMessage && !submitted && ( +
+ + + + {successMessage} +
+ )} + + {errorMessage && ( +
+ + + + {errorMessage} +
+ )} + + {submitted ? ( +
+
+ + + +
+

E-mail enviado com sucesso!

+

+ Um link de recuperação foi enviado para {email}. Clique no link para redefinir sua senha. +

+
+ + + Voltar para login + +
+
+ ) : ( + <> +
+

+ Digite o e-mail associado à sua conta e enviaremos um link para recuperar sua senha. +

+
+ +
+ +
+
+ @ +
+ setEmail(e.target.value)} + required + /> +
+
+ +
+ +
+ +
+ + Voltar para login + +
+ + )} +
+
+
+ ) +} diff --git a/frontend/src/pages/auth/Login.tsx b/frontend/src/pages/auth/Login.tsx index b55b291..324af35 100644 --- a/frontend/src/pages/auth/Login.tsx +++ b/frontend/src/pages/auth/Login.tsx @@ -1,9 +1,11 @@ import { FormEvent, useState } from 'react' +import { useNavigate } from 'react-router-dom' import axios from 'axios' import { useAuth, UserRole } from '@/context/AuthContext' import { authService } from '@/services/auth' import { logger } from '@/utils/logger' import { decodeJwtPayload } from '@/utils/jwt' +import { formatCNPJ, validateCNPJ } from '@/utils/cnpj' import logoImg from '@/assets/logo.png' // Ensure logo import is handled // Eye icon components for password visibility toggle @@ -22,12 +24,31 @@ const EyeSlashIcon = () => ( export function LoginPage() { const { login } = useAuth() + const navigate = useNavigate() + const [activeTab, setActiveTab] = useState<'login' | 'register'>('login') + + // Login form state const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [showPassword, setShowPassword] = useState(false) const [errorMessage, setErrorMessage] = useState(null) const [loading, setLoading] = useState(false) - const [activeTab, setActiveTab] = useState<'login' | 'register'>('login') + + // Register form state + const [registerData, setRegisterData] = useState({ + email: '', + username: '', + password: '', + passwordConfirm: '', + name: '', + company_name: '', + cnpj: '', + }) + const [showRegisterPassword, setShowRegisterPassword] = useState(false) + const [showRegisterPasswordConfirm, setShowRegisterPasswordConfirm] = useState(false) + const [registerLoading, setRegisterLoading] = useState(false) + const [registerErrorMessage, setRegisterErrorMessage] = useState(null) + const [registerValidationErrors, setRegisterValidationErrors] = useState>({}) const resolveRole = (role?: string): UserRole => { logger.info('🔐 [Login] Resolving role:', role) @@ -41,6 +62,108 @@ export function LoginPage() { } } + const handleRegisterChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + + if (name === 'cnpj') { + setRegisterData({ + ...registerData, + [name]: formatCNPJ(value), + }) + } else { + setRegisterData({ + ...registerData, + [name]: value, + }) + } + + // Clear validation error for this field + if (registerValidationErrors[name]) { + setRegisterValidationErrors({ + ...registerValidationErrors, + [name]: '', + }) + } + } + + const validateRegisterForm = (): boolean => { + const errors: Record = {} + + if (!registerData.email) errors.email = 'E-mail é obrigatório' + if (!registerData.email.includes('@')) errors.email = 'E-mail inválido' + + if (!registerData.username) errors.username = 'Usuário é obrigatório' + if (registerData.username.length < 3) errors.username = 'Usuário deve ter pelo menos 3 caracteres' + + if (!registerData.password) errors.password = 'Senha é obrigatória' + if (registerData.password.length < 8) errors.password = 'Senha deve ter pelo menos 8 caracteres' + + if (registerData.password !== registerData.passwordConfirm) errors.passwordConfirm = 'As senhas não correspondem' + + if (!registerData.name) errors.name = 'Nome é obrigatório' + if (registerData.name.length < 3) errors.name = 'Nome deve ter pelo menos 3 caracteres' + + if (!registerData.company_name) errors.company_name = 'Razão Social é obrigatória' + + if (!registerData.cnpj) { + errors.cnpj = 'CNPJ é obrigatório' + } else if (registerData.cnpj.replace(/\D/g, '').length !== 14) { + errors.cnpj = 'CNPJ deve ter 14 dígitos' + } else if (!validateCNPJ(registerData.cnpj)) { + errors.cnpj = 'CNPJ inválido. Verifique o dígito verificador' + } + + setRegisterValidationErrors(errors) + return Object.keys(errors).length === 0 + } + + const onRegisterSubmit = async (event: FormEvent) => { + event.preventDefault() + + if (!validateRegisterForm()) { + setRegisterErrorMessage('Por favor, corrija os erros no formulário.') + return + } + + setRegisterLoading(true) + setRegisterErrorMessage(null) + + try { + await authService.register({ + email: registerData.email, + username: registerData.username, + password: registerData.password, + name: registerData.name, + company_name: registerData.company_name, + cnpj: registerData.cnpj.replace(/\D/g, ''), + }) + + // Success - show success message and reset form + setRegisterData({ + email: '', + username: '', + password: '', + passwordConfirm: '', + name: '', + company_name: '', + cnpj: '', + }) + setActiveTab('login') + setRegisterErrorMessage(null) + } catch (error) { + const fallback = 'Não foi possível criar a conta. Tente novamente.' + if (axios.isAxiosError(error)) { + setRegisterErrorMessage(error.response?.data?.error ?? fallback) + } else if (error instanceof Error) { + setRegisterErrorMessage(error.message) + } else { + setRegisterErrorMessage(fallback) + } + } finally { + setRegisterLoading(false) + } + } + const onSubmit = async (event: FormEvent) => { event.preventDefault() setLoading(true) @@ -155,6 +278,12 @@ export function LoginPage() {
+ +
-
+
+ {registerErrorMessage && ( +
+ + + + {registerErrorMessage} +
+ )} + + {/* Personal Information */} +
+

Informações Pessoais

+ +
+
+ + + {registerValidationErrors.name &&

{registerValidationErrors.name}

} +
+ +
+ + + {registerValidationErrors.email &&

{registerValidationErrors.email}

} +
+ +
+ + + {registerValidationErrors.username &&

{registerValidationErrors.username}

} +
+
+
+ + {/* Company Information */} +
+

Informações da Empresa

+ +
+
+ + + {registerValidationErrors.company_name &&

{registerValidationErrors.company_name}

} +
+ +
+ + + {registerValidationErrors.cnpj &&

{registerValidationErrors.cnpj}

} +
+
+
+ + {/* Password Information */} +
+

Definir Senha

+ +
+
+ +
+
+ +
+ + +
+ {registerValidationErrors.password &&

{registerValidationErrors.password}

} +
+ +
+ +
+
+ +
+ + +
+ {registerValidationErrors.passwordConfirm &&

{registerValidationErrors.passwordConfirm}

} +
+
+
+ +
+ +
+ +
+

+ Já tenho uma conta{' '} + +

+
+
)}
diff --git a/frontend/src/pages/auth/Register.tsx b/frontend/src/pages/auth/Register.tsx new file mode 100644 index 0000000..2f16da7 --- /dev/null +++ b/frontend/src/pages/auth/Register.tsx @@ -0,0 +1,341 @@ +import { FormEvent, useState } from 'react' +import { useNavigate, Link } from 'react-router-dom' +import axios from 'axios' +import { authService } from '@/services/auth' +import { formatCNPJ, validateCNPJ } from '@/utils/cnpj' +import logoImg from '@/assets/logo.png' + +interface RegistrationFormData { + email: string + username: string + password: string + passwordConfirm: string + name: string + company_name: string + cnpj: string +} + +export function RegisterPage() { + const navigate = useNavigate() + const [formData, setFormData] = useState({ + email: '', + username: '', + password: '', + passwordConfirm: '', + name: '', + company_name: '', + cnpj: '', + }) + const [showPassword, setShowPassword] = useState(false) + const [showPasswordConfirm, setShowPasswordConfirm] = useState(false) + const [loading, setLoading] = useState(false) + const [errorMessage, setErrorMessage] = useState(null) + const [validationErrors, setValidationErrors] = useState>({}) + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + + if (name === 'cnpj') { + setFormData({ + ...formData, + [name]: formatCNPJ(value), + }) + } else { + setFormData({ + ...formData, + [name]: value, + }) + } + + // Clear validation error for this field + if (validationErrors[name]) { + setValidationErrors({ + ...validationErrors, + [name]: '', + }) + } + } + + const validateForm = (): boolean => { + const errors: Record = {} + + if (!formData.email) errors.email = 'E-mail é obrigatório' + if (!formData.email.includes('@')) errors.email = 'E-mail inválido' + + if (!formData.username) errors.username = 'Usuário é obrigatório' + if (formData.username.length < 3) errors.username = 'Usuário deve ter pelo menos 3 caracteres' + + if (!formData.password) errors.password = 'Senha é obrigatória' + if (formData.password.length < 8) errors.password = 'Senha deve ter pelo menos 8 caracteres' + + if (formData.password !== formData.passwordConfirm) errors.passwordConfirm = 'As senhas não correspondem' + + if (!formData.name) errors.name = 'Nome é obrigatório' + if (formData.name.length < 3) errors.name = 'Nome deve ter pelo menos 3 caracteres' + + if (!formData.company_name) errors.company_name = 'Razão Social é obrigatória' + + if (!formData.cnpj) { + errors.cnpj = 'CNPJ é obrigatório' + } else if (formData.cnpj.replace(/\D/g, '').length !== 14) { + errors.cnpj = 'CNPJ deve ter 14 dígitos' + } else if (!validateCNPJ(formData.cnpj)) { + errors.cnpj = 'CNPJ inválido. Verifique o dígito verificador' + } + + setValidationErrors(errors) + return Object.keys(errors).length === 0 + } + + const onSubmit = async (event: FormEvent) => { + event.preventDefault() + + if (!validateForm()) { + setErrorMessage('Por favor, corrija os erros no formulário.') + return + } + + setLoading(true) + setErrorMessage(null) + + try { + await authService.register({ + email: formData.email, + username: formData.username, + password: formData.password, + name: formData.name, + company_name: formData.company_name, + cnpj: formData.cnpj.replace(/\D/g, ''), + }) + + // Show success and redirect to login + navigate('/login?registered=true') + } catch (error) { + const fallback = 'Não foi possível criar a conta. Tente novamente.' + if (axios.isAxiosError(error)) { + setErrorMessage(error.response?.data?.error ?? fallback) + } else if (error instanceof Error) { + setErrorMessage(error.message) + } else { + setErrorMessage(fallback) + } + } finally { + setLoading(false) + } + } + + return ( +
+
+ {/* Blue Header with Logo */} +
+
+ Logo +
+

SaveInMed

+

Criar Nova Conta

+
+ + {/* Form Content */} +
+ {errorMessage && ( +
+ + + + {errorMessage} +
+ )} + + {/* Personal Information */} +
+

Informações Pessoais

+ +
+
+ + + {validationErrors.name &&

{validationErrors.name}

} +
+ +
+ + + {validationErrors.email &&

{validationErrors.email}

} +
+ +
+ + + {validationErrors.username &&

{validationErrors.username}

} +
+
+
+ + {/* Company Information */} +
+

Informações da Empresa

+ +
+
+ + + {validationErrors.company_name &&

{validationErrors.company_name}

} +
+ +
+ + + {validationErrors.cnpj &&

{validationErrors.cnpj}

} +
+
+
+ + {/* Password Information */} +
+

Definir Senha

+ +
+
+ +
+
+ +
+ + +
+ {validationErrors.password &&

{validationErrors.password}

} +
+ +
+ +
+
+ +
+ + +
+ {validationErrors.passwordConfirm &&

{validationErrors.passwordConfirm}

} +
+
+
+ +
+ +
+ +
+

+ Já tenho uma conta{' '} + + Entrar + +

+
+
+
+
+ ) +} diff --git a/frontend/src/pages/dashboard/admin/CompaniesPage.tsx b/frontend/src/pages/dashboard/admin/CompaniesPage.tsx index 17e295a..a2fe836 100644 --- a/frontend/src/pages/dashboard/admin/CompaniesPage.tsx +++ b/frontend/src/pages/dashboard/admin/CompaniesPage.tsx @@ -1,6 +1,8 @@ import { useEffect, useState } from 'react' import { adminService, Company, CreateCompanyRequest } from '@/services/adminService' import { useCepLookup } from '@/hooks/useCepLookup' +import { formatCNPJ, validateCNPJ } from '@/utils/cnpj' +import { formatPhone } from '@/utils/phone' export function CompaniesPage() { const [companies, setCompanies] = useState([]) @@ -14,14 +16,19 @@ export function CompaniesPage() { const [formData, setFormData] = useState({ cnpj: '', corporate_name: '', + fantasy_name: '', category: 'farmacia', license_number: '', latitude: -16.3281, longitude: -48.9530, city: 'Anápolis', - state: 'GO' + state: 'GO', + phone: '', + email: '', + founded_at: '', }) const [cep, setCep] = useState('') + const [cnpjError, setCnpjError] = useState(null) const { loading: cepLoading, error: cepError, lookup } = useCepLookup() const handleCepChange = async (value: string) => { @@ -62,15 +69,39 @@ export function CompaniesPage() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() + + // Only validate CNPJ on create (not on edit, since existing CNPJs may be legacy/test values) + if (!editingCompany) { + if (!formData.cnpj) { + setCnpjError('CNPJ é obrigatório') + return + } + const cnpjDigits = formData.cnpj.replace(/\D/g, '') + if (cnpjDigits.length !== 14) { + setCnpjError('CNPJ deve ter 14 dígitos') + return + } + if (!validateCNPJ(formData.cnpj)) { + setCnpjError('CNPJ inválido. Verifique o dígito verificador') + return + } + } + + // Strip CNPJ formatting before sending to API + const payload = { + ...formData, + cnpj: formData.cnpj.replace(/\D/g, ''), + } + try { if (editingCompany) { - await adminService.updateCompany(editingCompany.id, formData) + await adminService.updateCompany(editingCompany.id, payload) } else { - await adminService.createCompany(formData) + await adminService.createCompany(payload) } setShowModal(false) resetForm() - loadCompanies() + void loadCompanies() } catch (err) { console.error('Error saving company:', err) alert('Erro ao salvar empresa') @@ -99,38 +130,52 @@ export function CompaniesPage() { const openEdit = async (company: Company) => { setEditingCompany(company) + setCnpjError(null) setFormData({ - cnpj: company.cnpj, - corporate_name: company.corporate_name, - category: company.category, - license_number: company.license_number, - latitude: company.latitude, - longitude: company.longitude, - city: company.city, - state: company.state + cnpj: formatCNPJ(company.cnpj ?? ''), + corporate_name: company.corporate_name ?? '', + fantasy_name: company.fantasy_name ?? '', + category: company.category ?? 'farmacia', + license_number: company.license_number ?? '', + latitude: typeof company.latitude === 'number' ? company.latitude : -16.3281, + longitude: typeof company.longitude === 'number' ? company.longitude : -48.9530, + city: company.city ?? '', + state: company.state ?? '', + phone: company.phone ?? '', + email: company.email ?? '', + founded_at: company.founded_at + ? company.founded_at.slice(0, 10) + : '', }) setShowModal(true) try { const docs = await adminService.getCompanyDocuments(company.id) - setCompanyDocuments(docs) + setCompanyDocuments(Array.isArray(docs) ? docs : []) } catch (err) { console.error('Failed to load company docs', err) + setCompanyDocuments([]) } } const resetForm = () => { setEditingCompany(null) setCep('') + setCnpjError(null) + setCompanyDocuments([]) setFormData({ cnpj: '', corporate_name: '', + fantasy_name: '', category: 'farmacia', license_number: '', latitude: -16.3281, longitude: -48.9530, city: 'Anápolis', - state: 'GO' + state: 'GO', + phone: '', + email: '', + founded_at: '', }) } @@ -176,7 +221,8 @@ export function CompaniesPage() {
@@ -185,7 +231,7 @@ export function CompaniesPage() { {/* Table */}
- + @@ -278,51 +324,125 @@ export function CompaniesPage() { {/* Modal */} {showModal && ( -
+

{editingCompany ? 'Editar Empresa' : 'Nova Empresa'}

+ {/* CNPJ */} +
+ + { + const formatted = formatCNPJ(e.target.value) + setFormData({ ...formData, cnpj: formatted }) + if (cnpjError) setCnpjError(null) + }} + maxLength={18} + placeholder="00.000.000/0000-00" + disabled={!!editingCompany} + className={`mt-1 w-full rounded border px-3 py-2 font-mono focus:ring-1 outline-none ${editingCompany + ? 'border-gray-200 bg-gray-100 text-gray-500 cursor-not-allowed' + : cnpjError + ? 'border-red-500 bg-red-50 focus:border-red-500 focus:ring-red-500' + : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500' + }`} + required={!editingCompany} + /> + {cnpjError && ( +

{cnpjError}

+ )} +
+ + {/* Nome Fantasia */} +
+ + setFormData({ ...formData, fantasy_name: e.target.value })} + className="mt-1 w-full rounded border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" + placeholder="Nome fantasia da empresa" + /> +
+ + {/* Razão Social */}
setFormData({ ...formData, corporate_name: e.target.value })} - className="mt-1 w-full rounded border px-3 py-2" + className="mt-1 w-full rounded border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" required />
+ + {/* Data de Abertura */}
- + setFormData({ ...formData, cnpj: e.target.value })} - className="mt-1 w-full rounded border px-3 py-2" - required + type="date" + value={formData.founded_at || ''} + onChange={(e) => setFormData({ ...formData, founded_at: e.target.value })} + className="mt-1 w-full rounded border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" />
+ + {/* Categoria */}
+ + {/* Telefone */} +
+ + setFormData({ ...formData, phone: formatPhone(e.target.value) })} + placeholder="(00) 00000-0000" + maxLength={15} + className="mt-1 w-full rounded border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" + /> +
+ + {/* Email */} +
+ + setFormData({ ...formData, email: e.target.value })} + placeholder="contato@empresa.com.br" + className="mt-1 w-full rounded border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" + /> +
+ + {/* Licença Sanitária */}
setFormData({ ...formData, license_number: e.target.value })} - className="mt-1 w-full rounded border px-3 py-2" + className="mt-1 w-full rounded border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" required />
@@ -358,15 +478,15 @@ export function CompaniesPage() { file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100" /> - {companyDocuments.length > 0 && ( + {(companyDocuments ?? []).length > 0 && ( @@ -400,7 +520,7 @@ export function CompaniesPage() { type="text" value={formData.city} onChange={(e) => setFormData({ ...formData, city: e.target.value })} - className="mt-1 w-full rounded border px-3 py-2" + className="mt-1 w-full rounded border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" required />
@@ -410,7 +530,7 @@ export function CompaniesPage() { type="text" value={formData.state} onChange={(e) => setFormData({ ...formData, state: e.target.value })} - className="mt-1 w-full rounded border px-3 py-2" + className="mt-1 w-full rounded border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" maxLength={2} required /> @@ -420,9 +540,12 @@ export function CompaniesPage() { setFormData({ ...formData, latitude: parseFloat(e.target.value) })} - className="mt-1 w-full rounded border px-3 py-2" + value={Number.isFinite(formData.latitude) ? formData.latitude : ''} + onChange={(e) => { + const v = parseFloat(e.target.value) + setFormData({ ...formData, latitude: Number.isFinite(v) ? v : 0 }) + }} + className="mt-1 w-full rounded border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" required />
@@ -431,9 +554,12 @@ export function CompaniesPage() { setFormData({ ...formData, longitude: parseFloat(e.target.value) })} - className="mt-1 w-full rounded border px-3 py-2" + value={Number.isFinite(formData.longitude) ? formData.longitude : ''} + onChange={(e) => { + const v = parseFloat(e.target.value) + setFormData({ ...formData, longitude: Number.isFinite(v) ? v : 0 }) + }} + className="mt-1 w-full rounded border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" required />
@@ -448,7 +574,8 @@ export function CompaniesPage() { diff --git a/frontend/src/pages/dashboard/admin/DashboardHome.tsx b/frontend/src/pages/dashboard/admin/DashboardHome.tsx index 61539ef..8c2fe61 100644 --- a/frontend/src/pages/dashboard/admin/DashboardHome.tsx +++ b/frontend/src/pages/dashboard/admin/DashboardHome.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react' import { apiClient } from '@/services/apiClient' +import { FaUserGroup, FaBuilding, FaBoxOpen, FaClipboardList } from 'react-icons/fa6' interface DashboardStats { totalUsers: number @@ -76,10 +77,10 @@ export function DashboardHome() { } const cards = [ - { title: 'Usuários', value: stats.totalUsers, icon: '👥', color: 'from-blue-500 to-blue-600' }, - { title: 'Empresas', value: stats.totalCompanies, icon: '🏢', color: 'from-purple-500 to-purple-600' }, - { title: 'Produtos', value: stats.totalProducts, icon: '💊', color: 'from-green-500 to-green-600' }, - { title: 'Pedidos', value: stats.totalOrders, icon: '📦', color: 'from-orange-500 to-orange-600' } + { title: 'Usuários', value: stats.totalUsers, icon: }, + { title: 'Empresas', value: stats.totalCompanies, icon: }, + { title: 'Produtos', value: stats.totalProducts, icon: }, + { title: 'Pedidos', value: stats.totalOrders, icon: }, ] return ( @@ -91,16 +92,16 @@ export function DashboardHome() { {cards.map((card) => (
-

{card.title}

-

+

{card.title}

+

{loading ? '...' : card.value.toLocaleString('pt-BR')}

- {card.icon} + {card.icon}
))} diff --git a/frontend/src/pages/dashboard/admin/LogisticsPage.tsx b/frontend/src/pages/dashboard/admin/LogisticsPage.tsx index 1be125c..e8fab08 100644 --- a/frontend/src/pages/dashboard/admin/LogisticsPage.tsx +++ b/frontend/src/pages/dashboard/admin/LogisticsPage.tsx @@ -60,7 +60,7 @@ export function LogisticsPage() { {/* Table */}
Razão Social CNPJ
- + diff --git a/frontend/src/pages/dashboard/admin/OrdersPage.tsx b/frontend/src/pages/dashboard/admin/OrdersPage.tsx index 39c1795..67bc55f 100644 --- a/frontend/src/pages/dashboard/admin/OrdersPage.tsx +++ b/frontend/src/pages/dashboard/admin/OrdersPage.tsx @@ -85,7 +85,7 @@ export function OrdersPage() { {/* Table */}
Data Transportadora
- + diff --git a/frontend/src/pages/dashboard/admin/ProductsPage.tsx b/frontend/src/pages/dashboard/admin/ProductsPage.tsx index 9101cd3..7cf477b 100644 --- a/frontend/src/pages/dashboard/admin/ProductsPage.tsx +++ b/frontend/src/pages/dashboard/admin/ProductsPage.tsx @@ -160,7 +160,8 @@ export function ProductsPage() {

Produtos

@@ -169,7 +170,7 @@ export function ProductsPage() { {/* Table */}
ID Data
- + @@ -276,7 +277,7 @@ export function ProductsPage() { {/* Modal */} {showModal && ( -
+

{editingProduct ? 'Editar Produto' : 'Novo Produto'} @@ -372,7 +373,8 @@ export function ProductsPage() { diff --git a/frontend/src/pages/dashboard/admin/ProfilePage.tsx b/frontend/src/pages/dashboard/admin/ProfilePage.tsx index 94b1907..8a69f92 100644 --- a/frontend/src/pages/dashboard/admin/ProfilePage.tsx +++ b/frontend/src/pages/dashboard/admin/ProfilePage.tsx @@ -144,7 +144,8 @@ export function ProfilePage() { diff --git a/frontend/src/pages/dashboard/admin/ReviewsPage.tsx b/frontend/src/pages/dashboard/admin/ReviewsPage.tsx index 48ee88a..45725d2 100644 --- a/frontend/src/pages/dashboard/admin/ReviewsPage.tsx +++ b/frontend/src/pages/dashboard/admin/ReviewsPage.tsx @@ -59,7 +59,7 @@ export function ReviewsPage() { {/* Table */}

Produto Loja
- + diff --git a/frontend/src/pages/dashboard/admin/ShippingSettings.tsx b/frontend/src/pages/dashboard/admin/ShippingSettings.tsx index da5142f..b5ecce3 100644 --- a/frontend/src/pages/dashboard/admin/ShippingSettings.tsx +++ b/frontend/src/pages/dashboard/admin/ShippingSettings.tsx @@ -3,6 +3,8 @@ import { MapContainer, TileLayer, Circle, useMapEvents, Marker, Popup } from 're import 'leaflet/dist/leaflet.css' import { adminService, ShippingSettings } from '@/services/adminService' import { useAuth } from '@/context/AuthContext' +import { FaTruck, FaLocationDot, FaStore, FaCircleInfo, FaFloppyDisk } from 'react-icons/fa6' +import { Shell } from '@/layouts/Shell' import L from 'leaflet' @@ -103,166 +105,178 @@ export function ShippingSettingsPage() { const center: [number, number] = [settings.latitude, settings.longitude] - if (loading) return
Carregando configurações de frete...
+ if (loading) return ( + +
Carregando configurações de frete...
+
+ ) return ( -
-

- 🚚 - Configurações de Entrega e Retirada -

- - - - {/* Delivery Settings */} -
-
-

- 📍 - Entrega Própria -

- + +
+
+
+
+
+

Configurações de Entrega e Retirada

+

Defina as opções de frete e retirada da sua loja

+
+
-
-
- - setSettings({ ...settings, max_radius_km: parseFloat(e.target.value) })} - /> -

Clique no mapa para definir o raio visualmente.

+ + + {/* Delivery Settings */} +
+
+

+ + Entrega Própria +

+
-
+
- -
- R$ - setSettings({ ...settings, price_per_km_cents: Math.round(parseFloat(e.target.value) * 100) })} - /> -
-
-
- -
- R$ - setSettings({ ...settings, min_fee_cents: Math.round(parseFloat(e.target.value) * 100) })} - /> -
-
-
- -
- -
- R$ + setSettings({ ...settings, free_shipping_threshold_cents: Math.round(parseFloat(e.target.value) * 100) })} + step="0.1" + className="w-full p-2 border rounded" + value={settings.max_radius_km} + onChange={e => setSettings({ ...settings, max_radius_km: parseFloat(e.target.value) })} /> +

Clique no mapa para definir o raio visualmente.

+
+ +
+
+ +
+ R$ + setSettings({ ...settings, price_per_km_cents: Math.round(parseFloat(e.target.value) * 100) })} + /> +
+
+
+ +
+ R$ + setSettings({ ...settings, min_fee_cents: Math.round(parseFloat(e.target.value) * 100) })} + /> +
+
+
+ +
+ +
+ R$ + setSettings({ ...settings, free_shipping_threshold_cents: Math.round(parseFloat(e.target.value) * 100) })} + /> +
+
+
+ + {/* MAP AREA */} +
+ {settings.latitude !== 0 && ( + + + + Sua Loja + + {settings.active && ( + + )} + {settings.active && ( + setSettings(s => ({ ...s, max_radius_km: r }))} + /> + )} + + )} +
+
+ + {/* Pickup Settings */} +
+
+

+ + Retirada na Loja +

+ +
+ +
+
+ +
Data Pedido