atualizacao: tasks feitas - frontend
This commit is contained in:
parent
90467db1ec
commit
71d6a17dac
17 changed files with 1306 additions and 253 deletions
71
marketplace/package-lock.json
generated
71
marketplace/package-lock.json
generated
|
|
@ -90,15 +90,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector": {
|
||||
<<<<<<< HEAD
|
||||
"version": "6.7.8",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.8.tgz",
|
||||
"integrity": "sha512-stisC1nULNc9oH5lakAj8MH88ZxeGxzyWNDfbdCxvJSJIvDsHNZqYvscGTgy/ysgXWLJPt6K/4t0/GjvtKcFJQ==",
|
||||
=======
|
||||
"version": "6.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz",
|
||||
"integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==",
|
||||
>>>>>>> b6ef5f1 (feat: corrige login e adiciona documentação de regras de negócio)
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -106,11 +100,7 @@
|
|||
"bidi-js": "^1.0.3",
|
||||
"css-tree": "^3.1.0",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
<<<<<<< HEAD
|
||||
"lru-cache": "^11.2.5"
|
||||
=======
|
||||
"lru-cache": "^11.2.6"
|
||||
>>>>>>> b6ef5f1 (feat: corrige login e adiciona documentação de regras de negócio)
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": {
|
||||
|
|
@ -161,6 +151,7 @@
|
|||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
|
|
@ -452,15 +443,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/css-calc": {
|
||||
<<<<<<< HEAD
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.0.0.tgz",
|
||||
"integrity": "sha512-q4d82GTl8BIlh/dTnVsWmxnbWJeb3kiU8eUH71UxlxnS+WIaALmtzTL8gR15PkYOexMQYVk0CO4qIG93C1IvPA==",
|
||||
=======
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz",
|
||||
"integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==",
|
||||
>>>>>>> b6ef5f1 (feat: corrige login e adiciona documentação de regras de negócio)
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -525,6 +510,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
|
|
@ -565,6 +551,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
|
|
@ -1012,15 +999,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@exodus/bytes": {
|
||||
<<<<<<< HEAD
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.12.0.tgz",
|
||||
"integrity": "sha512-BuCOHA/EJdPN0qQ5MdgAiJSt9fYDHbghlgrj33gRdy/Yp1/FMCDhU6vJfcKrLC0TPWGSrfH3vYXBQWmFHxlddw==",
|
||||
=======
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.14.1.tgz",
|
||||
"integrity": "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==",
|
||||
>>>>>>> b6ef5f1 (feat: corrige login e adiciona documentação de regras de negócio)
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -1621,8 +1602,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
|
|
@ -1722,6 +1702,7 @@
|
|||
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
|
|
@ -1733,6 +1714,7 @@
|
|||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
|
|
@ -1899,7 +1881,6 @@
|
|||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
|
|
@ -1910,7 +1891,6 @@
|
|||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
|
|
@ -2040,13 +2020,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.9.19",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
|
||||
"integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
|
||||
"integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
"baseline-browser-mapping": "dist/cli.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bidi-js": {
|
||||
|
|
@ -2105,6 +2088,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
|
|
@ -2143,9 +2127,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001769",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz",
|
||||
"integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==",
|
||||
"version": "1.0.30001770",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz",
|
||||
"integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -2394,8 +2378,7 @@
|
|||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
|
|
@ -2972,6 +2955,7 @@
|
|||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
|
|
@ -2988,6 +2972,7 @@
|
|||
"integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@acemir/cssom": "^0.9.28",
|
||||
"@asamuzakjp/dom-selector": "^6.7.6",
|
||||
|
|
@ -3052,7 +3037,8 @@
|
|||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||
"license": "BSD-2-Clause"
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lilconfig": {
|
||||
"version": "3.1.3",
|
||||
|
|
@ -3111,7 +3097,6 @@
|
|||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
|
|
@ -3417,6 +3402,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
|
|
@ -3566,7 +3552,6 @@
|
|||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
|
|
@ -3618,6 +3603,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
|
|
@ -3630,6 +3616,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
|
|
@ -3643,8 +3630,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-leaflet": {
|
||||
"version": "4.2.1",
|
||||
|
|
@ -4118,6 +4104,7 @@
|
|||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -4268,6 +4255,7 @@
|
|||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
|
|
@ -4328,6 +4316,7 @@
|
|||
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.18",
|
||||
"@vitest/mocker": "4.0.18",
|
||||
|
|
@ -4884,6 +4873,7 @@
|
|||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -4897,6 +4887,7 @@
|
|||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
|
|
|||
|
|
@ -28,10 +28,17 @@ import {
|
|||
ShippingSettingsPage
|
||||
} from './pages/admin'
|
||||
|
||||
import { ForgotPasswordPage } from './pages/ForgotPasswordPage'
|
||||
import { RegisterPage } from './pages/RegisterPage'
|
||||
import { CompleteRegistrationPage } from './pages/CompleteRegistrationPage'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
|
||||
<Route path="/register" element={<RegisterPage />} />
|
||||
<Route path="/complete-registration" element={<CompleteRegistrationPage />} />
|
||||
|
||||
{/* Admin Dashboard with Header Layout */}
|
||||
<Route
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
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 = [
|
||||
{ path: '/dashboard', label: 'Início' },
|
||||
|
|
@ -32,14 +33,12 @@ export function Header() {
|
|||
}, [])
|
||||
|
||||
return (
|
||||
<header className="bg-gradient-to-r from-blue-900 to-blue-700 text-white shadow-lg">
|
||||
<div className="mx-auto max-w-7xl px-4">
|
||||
<header className="bg-[#1E3A8A] text-white shadow-md border-b border-blue-800">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex h-16 items-center justify-between">
|
||||
{/* Logo */}
|
||||
<Link to="/dashboard" className="flex items-center gap-2">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-white/20">
|
||||
<span className="text-xl font-bold">💊</span>
|
||||
</div>
|
||||
<img src={logoImg} alt="SaveInMed Logo" className="h-10 w-auto" />
|
||||
<span className="text-xl font-bold">SaveInMed</span>
|
||||
</Link>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,12 +13,13 @@ export interface AuthUser {
|
|||
companyId?: string
|
||||
role: UserRole
|
||||
token: string
|
||||
tax_id?: string
|
||||
}
|
||||
|
||||
interface AuthContextValue {
|
||||
user: AuthUser | null
|
||||
loading: boolean
|
||||
login: (token: string, role: UserRole, name: string, id: string, companyId?: string, email?: string, username?: string) => void
|
||||
login: (token: string, role: UserRole, name: string, id: string, companyId?: string, email?: string, username?: string, tax_id?: string) => void
|
||||
logout: () => void
|
||||
setUser: (user: AuthUser) => void
|
||||
}
|
||||
|
|
@ -53,8 +54,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
}, [user])
|
||||
|
||||
const login = (token: string, role: UserRole, name: string, id: string, companyId?: string, email?: string, username?: string) => {
|
||||
setUser({ token, role, name, id, companyId, email, username })
|
||||
const login = (token: string, role: UserRole, name: string, id: string, companyId?: string, email?: string, username?: string, tax_id?: string) => {
|
||||
setUser({
|
||||
token, role, name, id, companyId, email, username, tax_id
|
||||
})
|
||||
|
||||
// Redirect based on role
|
||||
switch (role) {
|
||||
|
|
|
|||
|
|
@ -71,7 +71,13 @@ export function Shell({ children }: { children: React.ReactNode }) {
|
|||
const { totalItems: cartCount } = useCartStore(selectCartSummary)
|
||||
|
||||
const isOwner = user?.role === 'owner' || user?.role === 'seller'
|
||||
const isEmployee = user?.role === 'employee'
|
||||
const isAdmin = user?.role === 'admin'
|
||||
|
||||
const showDashboard = isOwner
|
||||
const showOrders = isOwner || isEmployee
|
||||
const showProducts = isOwner || isEmployee
|
||||
|
||||
const profilePath = isAdmin ? '/dashboard/profile' : '/meu-perfil'
|
||||
const settingsPath = isOwner ? '/company' : '/dashboard/profile'
|
||||
|
||||
|
|
@ -96,7 +102,7 @@ export function Shell({ children }: { children: React.ReactNode }) {
|
|||
<div>
|
||||
<p className="text-lg font-semibold">SaveInMed</p>
|
||||
<p className="text-sm text-gray-100">
|
||||
{isAdmin ? 'Painel Administrativo' : isOwner ? 'Painel do Dono' : 'Marketplace B2B'}
|
||||
{isAdmin ? 'Painel Administrativo' : isOwner ? 'Painel do Dono' : isEmployee ? 'Painel do Colaborador' : 'Marketplace B2B'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -106,19 +112,25 @@ export function Shell({ children }: { children: React.ReactNode }) {
|
|||
Admin
|
||||
</Link>
|
||||
)}
|
||||
{isOwner && (
|
||||
<>
|
||||
|
||||
{showDashboard && (
|
||||
<Link to="/seller" className="hover:underline">
|
||||
Dashboard
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{showOrders && (
|
||||
<Link to="/orders" className="hover:underline">
|
||||
Meus Pedidos
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{showProducts && (
|
||||
<Link to="/inventory" className="hover:underline">
|
||||
Meus Produtos
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Cart with hover dropdown */}
|
||||
<div className="relative group">
|
||||
<Link
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { maskCPF } from '../utils/validators'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Shell } from '../layouts/Shell'
|
||||
import { useCartStore, selectGroupedCart, selectCartSummary } from '../stores/cartStore'
|
||||
|
|
@ -30,7 +31,8 @@ export function CheckoutPage() {
|
|||
city: '',
|
||||
state: '',
|
||||
zip_code: '',
|
||||
country: 'Brasil'
|
||||
country: 'Brasil',
|
||||
tax_id: user?.tax_id || ''
|
||||
})
|
||||
|
||||
// Pre-fill address from company
|
||||
|
|
@ -90,9 +92,12 @@ export function CheckoutPage() {
|
|||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target
|
||||
setPaymentMethod(prev => prev) // Keep state if needed, though strictly not needed for this simple setter
|
||||
if (name === 'tax_id') {
|
||||
setShipping(prev => ({ ...prev, [name]: maskCPF(value) }))
|
||||
} else {
|
||||
setShipping(prev => ({ ...prev, [name]: value }))
|
||||
}
|
||||
}
|
||||
|
||||
const handlePlaceOrder = async () => {
|
||||
if (!user) return
|
||||
|
|
@ -120,7 +125,8 @@ export function CheckoutPage() {
|
|||
city: shipping.city,
|
||||
state: shipping.state,
|
||||
zip_code: shipping.zip_code,
|
||||
country: shipping.country
|
||||
country: shipping.country,
|
||||
tax_id: shipping.tax_id
|
||||
},
|
||||
payment_method: paymentMethod
|
||||
}
|
||||
|
|
@ -181,6 +187,18 @@ export function CheckoutPage() {
|
|||
className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-medicalBlue focus:outline-none focus:ring-1 focus:ring-medicalBlue"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">CPF</label>
|
||||
<input
|
||||
type="text"
|
||||
name="tax_id"
|
||||
value={shipping.tax_id || ''}
|
||||
onChange={handleInputChange}
|
||||
className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-medicalBlue focus:outline-none focus:ring-1 focus:ring-medicalBlue"
|
||||
placeholder="000.000.000-00"
|
||||
maxLength={14}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">CEP</label>
|
||||
<input
|
||||
|
|
|
|||
387
marketplace/src/pages/CompleteRegistrationPage.tsx
Normal file
387
marketplace/src/pages/CompleteRegistrationPage.tsx
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { maskCNPJ, isValidCNPJ, maskCPF, isValidCPF, maskCEP } from '../utils/validators'
|
||||
|
||||
// Máscara local para telefone
|
||||
const maskPhone = (v: string) => v.replace(/\D/g, '').replace(/^(\d{2})(\d)/g, '($1) $2').replace(/(\d)(\d{4})$/, '$1-$2').substring(0, 15)
|
||||
|
||||
export function CompleteRegistrationPage() {
|
||||
const navigate = useNavigate()
|
||||
const [step, setStep] = useState(1) // 1: Pessoal, 2: Endereço, 3: Empresa
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// Erros
|
||||
const [cpfError, setCpfError] = useState('')
|
||||
const [cnpjError, setCnpjError] = useState('')
|
||||
const [cepError, setCepError] = useState('')
|
||||
const [cepLoading, setCepLoading] = useState(false)
|
||||
|
||||
// Dados Pessoais
|
||||
const [personalData, setPersonalData] = useState({
|
||||
nomeCivil: '',
|
||||
nomeSocial: '',
|
||||
cpf: '',
|
||||
})
|
||||
|
||||
// Endereço
|
||||
const [addressData, setAddressData] = useState({
|
||||
cep: '',
|
||||
logradouro: '',
|
||||
numero: '',
|
||||
complemento: '',
|
||||
bairro: '',
|
||||
cidade: '',
|
||||
estado: '',
|
||||
})
|
||||
|
||||
// Empresa
|
||||
const [companyData, setCompanyData] = useState({
|
||||
cnpj: '',
|
||||
razaoSocial: '',
|
||||
nomeFantasia: '',
|
||||
telefone: '',
|
||||
email: '',
|
||||
})
|
||||
|
||||
const handleBlurCPF = () => {
|
||||
if (personalData.cpf && !isValidCPF(personalData.cpf)) {
|
||||
setCpfError('CPF inválido')
|
||||
} else {
|
||||
setCpfError('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleBlurCNPJ = () => {
|
||||
if (companyData.cnpj && !isValidCNPJ(companyData.cnpj)) {
|
||||
setCnpjError('CNPJ inválido')
|
||||
} else {
|
||||
setCnpjError('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleBlurCEP = async () => {
|
||||
const rawCep = addressData.cep.replace(/\D/g, '')
|
||||
if (rawCep.length !== 8) {
|
||||
// Se estiver vazio não mostra erro, só se tiver incompleto
|
||||
if (rawCep.length > 0) setCepError('CEP incompleto')
|
||||
return
|
||||
}
|
||||
|
||||
setCepLoading(true)
|
||||
setCepError('')
|
||||
try {
|
||||
const response = await fetch(`https://viacep.com.br/ws/${rawCep}/json/`)
|
||||
const data = await response.json()
|
||||
if (data.erro) {
|
||||
setCepError('CEP não encontrado')
|
||||
return
|
||||
}
|
||||
setAddressData(prev => ({
|
||||
...prev,
|
||||
logradouro: data.logradouro,
|
||||
bairro: data.bairro,
|
||||
cidade: data.localidade,
|
||||
estado: data.uf,
|
||||
// Mantém número e complemento se já digitados? Geralmente CEP preenche logs...
|
||||
// Se complemento vier da API (raro em viacep genérico), preenche.
|
||||
}))
|
||||
} catch (err) {
|
||||
setCepError('Erro ao buscar CEP')
|
||||
} finally {
|
||||
setCepLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNext = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
// Validar passo atual antes de avançar
|
||||
if (step === 1) {
|
||||
if (cpfError || !personalData.nomeCivil || !isValidCPF(personalData.cpf)) {
|
||||
if (!isValidCPF(personalData.cpf)) setCpfError('CPF inválido')
|
||||
return
|
||||
}
|
||||
}
|
||||
if (step === 2) {
|
||||
if (cepError || !addressData.logradouro || !addressData.numero || !addressData.bairro || !addressData.cidade || !addressData.estado) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setStep(step + 1)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (cnpjError || !isValidCNPJ(companyData.cnpj)) {
|
||||
if (!isValidCNPJ(companyData.cnpj)) setCnpjError('CNPJ inválido')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
// Simular envio
|
||||
console.log('Dados completos:', { personalData, addressData, companyData })
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
|
||||
alert('Cadastro completado com sucesso! Aguarde a aprovação.')
|
||||
navigate('/login')
|
||||
} catch (error) {
|
||||
alert('Erro ao salvar dados.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="bg-white shadow-xl rounded-lg overflow-hidden">
|
||||
<div className="bg-blue-600 px-6 py-4 text-white">
|
||||
<h1 className="text-2xl font-bold">Completar Registro</h1>
|
||||
<p className="text-blue-100 text-sm">Passo {step} de 3</p>
|
||||
</div>
|
||||
|
||||
<div className="p-8">
|
||||
{/* Progress Bar */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div className={`flex-1 h-2 rounded-full ${step >= 1 ? 'bg-blue-600' : 'bg-gray-200'}`}></div>
|
||||
<div className="w-2"></div>
|
||||
<div className={`flex-1 h-2 rounded-full ${step >= 2 ? 'bg-blue-600' : 'bg-gray-200'}`}></div>
|
||||
<div className="w-2"></div>
|
||||
<div className={`flex-1 h-2 rounded-full ${step >= 3 ? 'bg-blue-600' : 'bg-gray-200'}`}></div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={step === 3 ? handleSubmit : handleNext}>
|
||||
|
||||
{/* Step 1: Dados Pessoais */}
|
||||
{step === 1 && (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold text-gray-800">Dados Pessoais</h2>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Nome Civil</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
value={personalData.nomeCivil}
|
||||
onChange={e => setPersonalData({ ...personalData, nomeCivil: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">CPF</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-1 ${cpfError ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'}`}
|
||||
value={personalData.cpf}
|
||||
onChange={e => {
|
||||
setPersonalData({ ...personalData, cpf: maskCPF(e.target.value) })
|
||||
setCpfError('')
|
||||
}}
|
||||
onBlur={handleBlurCPF}
|
||||
placeholder="000.000.000-00"
|
||||
maxLength={14}
|
||||
/>
|
||||
{cpfError && <p className="mt-1 text-xs text-red-500">{cpfError}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Endereço */}
|
||||
{step === 2 && (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold text-gray-800">Endereço</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700">CEP</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-1 ${cepError ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'}`}
|
||||
value={addressData.cep}
|
||||
onChange={e => {
|
||||
setAddressData({ ...addressData, cep: maskCEP(e.target.value) })
|
||||
setCepError('')
|
||||
}}
|
||||
onBlur={handleBlurCEP}
|
||||
placeholder="00000-000"
|
||||
maxLength={9}
|
||||
/>
|
||||
{cepLoading && (
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-blue-500 border-t-transparent"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{cepError && <p className="mt-1 text-xs text-red-500">{cepError}</p>}
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700">Logradouro</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 bg-gray-50 bg-opacity-50"
|
||||
value={addressData.logradouro}
|
||||
onChange={e => setAddressData({ ...addressData, logradouro: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Número</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
value={addressData.numero}
|
||||
onChange={e => setAddressData({ ...addressData, numero: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Complemento</label>
|
||||
<input
|
||||
type="text"
|
||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
value={addressData.complemento}
|
||||
onChange={e => setAddressData({ ...addressData, complemento: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Bairro</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 bg-gray-50 bg-opacity-50"
|
||||
value={addressData.bairro}
|
||||
onChange={e => setAddressData({ ...addressData, bairro: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Cidade</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 bg-gray-50 bg-opacity-50"
|
||||
value={addressData.cidade}
|
||||
onChange={e => setAddressData({ ...addressData, cidade: e.target.value })}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
className="mt-1 block w-20 rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 bg-gray-50 bg-opacity-50 text-center"
|
||||
value={addressData.estado}
|
||||
placeholder="UF"
|
||||
onChange={e => setAddressData({ ...addressData, estado: e.target.value })}
|
||||
maxLength={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Empresa */}
|
||||
{step === 3 && (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold text-gray-800">Dados da Empresa</h2>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">CNPJ</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-1 ${cnpjError ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'}`}
|
||||
value={companyData.cnpj}
|
||||
onChange={e => {
|
||||
setCompanyData({ ...companyData, cnpj: maskCNPJ(e.target.value) })
|
||||
setCnpjError('')
|
||||
}}
|
||||
onBlur={handleBlurCNPJ}
|
||||
placeholder="00.000.000/0000-00"
|
||||
maxLength={18}
|
||||
/>
|
||||
{cnpjError && <p className="mt-1 text-xs text-red-500">{cnpjError}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Razão Social</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
value={companyData.razaoSocial}
|
||||
onChange={e => setCompanyData({ ...companyData, razaoSocial: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Nome Fantasia</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
value={companyData.nomeFantasia}
|
||||
onChange={e => setCompanyData({ ...companyData, nomeFantasia: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Telefone</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
value={companyData.telefone}
|
||||
onChange={e => setCompanyData({ ...companyData, telefone: maskPhone(e.target.value) })}
|
||||
placeholder="(00) 00000-0000"
|
||||
maxLength={15}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Email da Empresa</label>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
value={companyData.email}
|
||||
onChange={e => setCompanyData({ ...companyData, email: e.target.value })}
|
||||
placeholder="contato@empresa.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8 flex justify-between">
|
||||
{step > 1 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep(step - 1)}
|
||||
className="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Voltar
|
||||
</button>
|
||||
) : <div></div>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="rounded-lg bg-blue-600 px-6 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Salvando...' : step === 3 ? 'Finalizar Cadastro' : 'Próximo'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/* Botão de Skip para teste (remover em prod) */}
|
||||
<div className="text-center mt-4">
|
||||
<button onClick={() => navigate('/login')} className="text-xs text-gray-400 hover:text-gray-600">
|
||||
Cancelar e voltar ao login
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,30 +1,187 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { CheckCircle, MapPin, Package, Truck, User } from 'lucide-react'
|
||||
// import { apiClient } from '../services/apiClient' // Descomentar quando integrar
|
||||
|
||||
// Mocks para desenvolvimento da interface
|
||||
const MOCK_ORDERS = [
|
||||
{
|
||||
id: '1',
|
||||
origin: 'Distribuidora ZL',
|
||||
destination: 'Farmácia Central',
|
||||
distance: '5.2 km',
|
||||
fee: 'R$ 15,00',
|
||||
status: 'ready', // pronto para entrega
|
||||
items: 12,
|
||||
address: 'Rua da Mooca, 123 - Mooca, SP'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
origin: 'Distribuidora Norte',
|
||||
destination: 'Drogasil Tatuapé',
|
||||
distance: '8.4 km',
|
||||
fee: 'R$ 22,50',
|
||||
status: 'ready',
|
||||
items: 45,
|
||||
address: 'Av. Celso Garcia, 4500 - Tatuapé, SP'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
origin: 'Distribuidora ZL',
|
||||
destination: 'Pague Menos Belém',
|
||||
distance: '2.1 km',
|
||||
fee: 'R$ 8,00',
|
||||
status: 'delivering', // em rota
|
||||
items: 5,
|
||||
address: 'Rua Belém, 50 - Belém, SP'
|
||||
}
|
||||
]
|
||||
|
||||
export function DeliveryDashboardPage() {
|
||||
const { user, logout } = useAuth()
|
||||
const [activeTab, setActiveTab] = useState<'available' | 'active'>('available')
|
||||
const [orders, setOrders] = useState(MOCK_ORDERS)
|
||||
|
||||
// Filtrar pedidos baseados na aba
|
||||
const displayedOrders = orders.filter(order =>
|
||||
activeTab === 'available' ? order.status === 'ready' : order.status === 'delivering'
|
||||
)
|
||||
|
||||
const handleAcceptDelivery = (id: string) => {
|
||||
// TODO: Chamar API para atribuir entrega
|
||||
setOrders(prev => prev.map(o => o.id === id ? { ...o, status: 'delivering' } : o))
|
||||
setActiveTab('active') // Mudar para aba de ativas
|
||||
}
|
||||
|
||||
const handleConfirmDelivery = (id: string) => {
|
||||
// TODO: Chamar API para finalizar entrega
|
||||
// Mock remove da lista de ativas (pois foi entregue/histórico)
|
||||
setOrders(prev => prev.filter(o => o.id !== id))
|
||||
alert("Entrega confirmada com sucesso!")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 p-8">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="flex items-center justify-between rounded-lg bg-white p-6 shadow">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Painel do Entregador</h1>
|
||||
<p className="text-gray-600">Bem-vindo, {user?.name}</p>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header Mobile-First Simplificado */}
|
||||
<header className="bg-white shadow">
|
||||
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-full bg-blue-100 p-2 text-blue-600">
|
||||
<Truck className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-900">Entregas</h1>
|
||||
<p className="text-xs text-gray-500">Olá, {user?.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={logout}
|
||||
className="rounded bg-red-600 px-4 py-2 font-bold text-white hover:bg-red-700"
|
||||
className="rounded-lg bg-red-50 px-3 py-1.5 text-sm font-medium text-red-600 hover:bg-red-100"
|
||||
>
|
||||
Sair
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="mt-8 rounded-lg bg-white p-6 shadow">
|
||||
<h3 className="text-lg font-bold">Minhas Entregas</h3>
|
||||
<p className="mt-2 text-gray-600">Visualize as entregas pendentes e o mapa de rotas.</p>
|
||||
{/* Map Integration would go here */}
|
||||
<main className="mx-auto max-w-lg px-4 py-6 sm:px-6">
|
||||
|
||||
{/* Tabs de Navegação */}
|
||||
<div className="mb-6 flex rounded-xl bg-white p-1 shadow-sm">
|
||||
<button
|
||||
onClick={() => setActiveTab('available')}
|
||||
className={`flex-1 rounded-lg py-2.5 text-sm font-medium transition-all ${activeTab === 'available'
|
||||
? 'bg-blue-600 text-white shadow-md'
|
||||
: 'text-gray-500 hover:bg-gray-50 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Disponíveis ({orders.filter(o => o.status === 'ready').length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('active')}
|
||||
className={`flex-1 rounded-lg py-2.5 text-sm font-medium transition-all ${activeTab === 'active'
|
||||
? 'bg-blue-600 text-white shadow-md'
|
||||
: 'text-gray-500 hover:bg-gray-50 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Minhas Entregas ({orders.filter(o => o.status === 'delivering').length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Lista de Cards */}
|
||||
<div className="space-y-4">
|
||||
{displayedOrders.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<Package className="mb-3 h-12 w-12 text-gray-300" />
|
||||
<p className="text-gray-500">Nenhuma entrega encontrada nesta aba.</p>
|
||||
</div>
|
||||
) : (
|
||||
displayedOrders.map((order) => (
|
||||
<div key={order.id} className="relative overflow-hidden rounded-xl bg-white p-5 shadow-sm ring-1 ring-gray-100">
|
||||
{/* Badge de Status */}
|
||||
<div className="absolute right-0 top-0 rounded-bl-xl bg-green-50 px-3 py-1 text-xs font-semibold text-green-700">
|
||||
{activeTab === 'available' ? 'Pronto para retirada' : 'Em rota'}
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-bold text-gray-900">{order.fee}</h3>
|
||||
<div className="mt-1 flex items-center text-sm text-gray-500">
|
||||
<MapPin className="mr-1.5 h-4 w-4 text-gray-400" />
|
||||
{order.distance} • {order.items} volumes
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rota */}
|
||||
<div className="space-y-3 border-t border-gray-100 pt-4">
|
||||
<div className="flex gap-3">
|
||||
<div className="mt-1">
|
||||
<div className="h-2 w-2 rounded-full bg-gray-300 ring-2 ring-white"></div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-medium text-gray-500">Retirada</p>
|
||||
<p className="text-sm font-semibold text-gray-900">{order.origin}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Linha vertical conectando os pontos */}
|
||||
<div className="ml-1 -mt-2 h-4 w-0.5 bg-gray-200"></div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<div className="mt-1">
|
||||
<div className="h-2 w-2 rounded-full bg-blue-600 ring-2 ring-blue-100"></div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-medium text-gray-500">Entrega</p>
|
||||
<p className="text-sm font-semibold text-gray-900">{order.destination}</p>
|
||||
<p className="text-xs text-gray-500">{order.address}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ações */}
|
||||
<div className="mt-5">
|
||||
{activeTab === 'available' ? (
|
||||
<button
|
||||
onClick={() => handleAcceptDelivery(order.id)}
|
||||
className="flex w-full items-center justify-center rounded-lg bg-blue-600 px-4 py-3 font-semibold text-white transition-colors hover:bg-blue-700"
|
||||
>
|
||||
Aceitar Entrega
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleConfirmDelivery(order.id)}
|
||||
className="flex w-full items-center justify-center rounded-lg bg-green-600 px-4 py-3 font-semibold text-white transition-colors hover:bg-green-700"
|
||||
>
|
||||
<CheckCircle className="mr-2 h-5 w-5" />
|
||||
Confirmar Entrega
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
133
marketplace/src/pages/ForgotPasswordPage.tsx
Normal file
133
marketplace/src/pages/ForgotPasswordPage.tsx
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ArrowLeft, Mail } from 'lucide-react'
|
||||
import logoImg from '../assets/logo.png'
|
||||
import { authService } from '../services/auth'
|
||||
|
||||
export function ForgotPasswordPage() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [sent, setSent] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
await authService.forgotPassword(email)
|
||||
setSent(true)
|
||||
} catch (err: any) {
|
||||
console.error('Erro ao recuperar senha:', err)
|
||||
if (err.response?.status === 404) {
|
||||
setSent(true)
|
||||
} else {
|
||||
setError('Não foi possível enviar o e-mail. Verifique se o endereço está correto.')
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<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 (Igual ao Login) */}
|
||||
<div className="flex flex-col items-center bg-blue-600 pb-8 pt-10 text-white">
|
||||
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-white/10 backdrop-blur-sm">
|
||||
<img src={logoImg} alt="Logo" className="h-10 w-auto brightness-0 invert" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold">SaveInMed</h1>
|
||||
<p className="text-blue-100 text-sm">Plataforma B2B de Medicamentos</p>
|
||||
</div>
|
||||
|
||||
<div className="p-8">
|
||||
{sent ? (
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-green-50 ring-8 ring-green-50/50">
|
||||
<Mail className="h-8 w-8 text-green-600" />
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-bold text-gray-900">Verifique seu e-mail</h3>
|
||||
<p className="mb-8 text-sm text-gray-500 leading-relaxed">
|
||||
Enviamos as instruções de recuperação de senha para <strong>{email}</strong>.
|
||||
</p>
|
||||
<Link
|
||||
to="/login"
|
||||
className="flex w-full items-center justify-center rounded-xl bg-slate-600 py-3 font-semibold text-white shadow-lg shadow-slate-200 hover:bg-slate-700 hover:shadow-xl transition-all"
|
||||
>
|
||||
Voltar para o Login
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="mb-6 text-center">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Esqueceu sua senha?</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Digite seu e-mail abaixo para receber o link de redefinição.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="space-y-5" onSubmit={handleSubmit}>
|
||||
{error && (
|
||||
<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">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="email" className="text-sm font-medium text-gray-700 ml-1">
|
||||
E-mail cadastrado
|
||||
</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="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
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"
|
||||
placeholder="seu@email.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
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"
|
||||
>
|
||||
{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>
|
||||
Enviando...
|
||||
</>
|
||||
) : (
|
||||
'Enviar instruções'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<Link
|
||||
to="/login"
|
||||
className="inline-flex items-center gap-2 text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Voltar para o Login
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ 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
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
// Eye icon components for password visibility toggle
|
||||
const EyeIcon = () => (
|
||||
|
|
@ -27,7 +28,7 @@ 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)
|
||||
|
|
@ -41,7 +42,7 @@ export function LoginPage() {
|
|||
}
|
||||
}
|
||||
|
||||
const onSubmit = async (event: FormEvent) => {
|
||||
const onSubmit = async (event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
setLoading(true)
|
||||
setErrorMessage(null)
|
||||
|
|
@ -91,26 +92,7 @@ const onSubmit = async (event: FormEvent) => {
|
|||
<p className="text-blue-100 text-sm">Plataforma B2B de Medicamentos</p>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-gray-100 p-2 bg-white">
|
||||
<button
|
||||
onClick={() => setActiveTab('login')}
|
||||
className={`flex-1 rounded-lg py-2 text-sm font-medium transition-colors ${activeTab === 'login' ? 'bg-white text-blue-600 shadow-sm border border-gray-100' : 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
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">
|
||||
|
|
@ -165,6 +147,12 @@ const onSubmit = async (event: FormEvent) => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<Link to="/forgot-password" className="text-sm font-medium text-blue-600 hover:text-blue-500">
|
||||
Esqueceu a senha?
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
|
|
@ -184,18 +172,16 @@ const onSubmit = async (event: FormEvent) => {
|
|||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="p-8 text-center text-gray-500 py-12">
|
||||
<p>Funcionalidade de cadastro em breve.</p>
|
||||
<button
|
||||
onClick={() => setActiveTab('login')}
|
||||
className="mt-4 text-blue-600 hover:underline text-sm"
|
||||
>
|
||||
Voltar para login
|
||||
</button>
|
||||
|
||||
<div className="mt-6 text-center text-sm">
|
||||
<p className="text-gray-500">
|
||||
Ainda não tem conta?{' '}
|
||||
<Link to="/register" className="font-semibold text-blue-600 hover:text-blue-500">
|
||||
Cadastre-se
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
201
marketplace/src/pages/RegisterPage.tsx
Normal file
201
marketplace/src/pages/RegisterPage.tsx
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
import { FormEvent, useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { authService } from '../services/auth'
|
||||
import logoImg from '../assets/logo.png'
|
||||
|
||||
// Definindo Ícones aqui para evitar dependência externa se não existir arquivo
|
||||
const EyeIconC = () => (
|
||||
<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 EyeSlashIconC = () => (
|
||||
<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>
|
||||
)
|
||||
|
||||
export function RegisterPage() {
|
||||
const navigate = useNavigate()
|
||||
const [name, setName] = useState('')
|
||||
const [username, setUsername] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError('As senhas não coincidem.')
|
||||
return
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
setError('A senha deve ter pelo menos 8 caracteres.')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const response = await authService.register({
|
||||
name,
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
role: 'owner' // Default role for new registration via web
|
||||
}) as any
|
||||
|
||||
// Assumindo que o register retorna token ou sucesso.
|
||||
// Se retornar token, logamos o usuário.
|
||||
// Se não, redirecionamos para login.
|
||||
|
||||
// Para manter simples e seguir o fluxo pedido:
|
||||
// "depois do cadastro e pra ter uma tela de completar registro"
|
||||
|
||||
// Se o backend retornar token, salvamos e redirecionamos.
|
||||
if (response.token || response.access_token) {
|
||||
localStorage.setItem('token', response.token || response.access_token)
|
||||
// Redireciona para completar registro
|
||||
navigate('/complete-registration')
|
||||
} else {
|
||||
// Se não retornar token auto-login, manda pro login
|
||||
alert('Cadastro realizado com sucesso! Faça login para continuar.')
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
setError(err.response?.data?.error || 'Erro ao realizar cadastro. Tente novamente.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
<h1 className="text-2xl font-bold">SaveInMed</h1>
|
||||
<p className="text-blue-100 text-sm">Plataforma B2B de Medicamentos</p>
|
||||
</div>
|
||||
|
||||
<div className="p-8">
|
||||
<div className="mb-6 text-center">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Crie sua conta</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Preencha os dados abaixo para começar
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
{error && (
|
||||
<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">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium text-gray-700 ml-1">Nome Completo</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 px-3 text-gray-800 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
placeholder="Seu nome"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium text-gray-700 ml-1">Nome de Usuário</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 px-3 text-gray-800 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
placeholder="usuario.exemplo"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium text-gray-700 ml-1">E-mail</label>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 px-3 text-gray-800 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
placeholder="seu@email.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium text-gray-700 ml-1">Senha</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
required
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 pl-3 pr-10 text-gray-800 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
placeholder="••••••••"
|
||||
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 ? <EyeSlashIconC /> : <EyeIconC />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium text-gray-700 ml-1">Confirmar Senha</label>
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 px-3 text-gray-800 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
placeholder="••••••••"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
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"
|
||||
>
|
||||
{loading ? 'Cadastrando...' : 'Criar conta'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-500">
|
||||
Já tem uma conta?{' '}
|
||||
<Link to="/login" className="font-medium text-blue-600 hover:text-blue-500 hover:underline">
|
||||
Entrar
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { adminService, Company, CreateCompanyRequest } from '../../services/adminService'
|
||||
import { isValidCNPJ, maskCNPJ } from '../../utils/validators'
|
||||
|
||||
export function CompaniesPage() {
|
||||
const [companies, setCompanies] = useState<Company[]>([])
|
||||
|
|
@ -19,6 +20,7 @@ export function CompaniesPage() {
|
|||
city: 'Anápolis',
|
||||
state: 'GO'
|
||||
})
|
||||
const [cnpjError, setCnpjError] = useState('')
|
||||
|
||||
const pageSize = 50
|
||||
|
||||
|
|
@ -41,6 +43,12 @@ export function CompaniesPage() {
|
|||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!isValidCNPJ(formData.cnpj)) {
|
||||
setCnpjError('CNPJ inválido')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (editingCompany) {
|
||||
await adminService.updateCompany(editingCompany.id, formData)
|
||||
|
|
@ -93,6 +101,7 @@ export function CompaniesPage() {
|
|||
|
||||
const resetForm = () => {
|
||||
setEditingCompany(null)
|
||||
setCnpjError('')
|
||||
setFormData({
|
||||
cnpj: '',
|
||||
corporate_name: '',
|
||||
|
|
@ -271,10 +280,22 @@ export function CompaniesPage() {
|
|||
<input
|
||||
type="text"
|
||||
value={formData.cnpj}
|
||||
onChange={(e) => setFormData({ ...formData, cnpj: e.target.value })}
|
||||
className="mt-1 w-full rounded border px-3 py-2"
|
||||
onChange={(e) => {
|
||||
const masked = maskCNPJ(e.target.value)
|
||||
setFormData({ ...formData, cnpj: masked })
|
||||
if (masked.length === 18 && !isValidCNPJ(masked)) {
|
||||
setCnpjError('CNPJ inválido')
|
||||
} else {
|
||||
setCnpjError('')
|
||||
}
|
||||
}}
|
||||
className={`mt-1 w-full rounded border px-3 py-2 ${cnpjError ? 'border-red-500 focus:ring-red-500' : 'border-gray-300'
|
||||
}`}
|
||||
maxLength={18}
|
||||
placeholder="00.000.000/0000-00"
|
||||
required
|
||||
/>
|
||||
{cnpjError && <p className="mt-1 text-xs text-red-500">{cnpjError}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Categoria</label>
|
||||
|
|
|
|||
|
|
@ -76,55 +76,70 @@ 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: '👥', color: 'text-blue-600 bg-blue-50' },
|
||||
{ title: 'Empresas', value: stats.totalCompanies, icon: '🏢', color: 'text-indigo-600 bg-indigo-50' },
|
||||
{ title: 'Produtos', value: stats.totalProducts, icon: '💊', color: 'text-emerald-600 bg-emerald-50' },
|
||||
{ title: 'Pedidos', value: stats.totalOrders, icon: '📦', color: 'text-amber-600 bg-amber-50' }
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="mb-6 text-2xl font-bold text-gray-900">Painel Administrativo</h1>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Visão Geral</h1>
|
||||
<p className="mt-1 text-sm text-gray-500">Acompanhe os indicadores principais da plataforma.</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
{cards.map((card) => (
|
||||
<div
|
||||
key={card.title}
|
||||
className={`rounded-xl bg-gradient-to-br ${card.color} p-6 text-white shadow-lg`}
|
||||
className="flex items-center justify-between rounded-xl border border-gray-100 bg-white p-6 shadow-sm transition-all hover:shadow-md"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white/80">{card.title}</p>
|
||||
<p className="mt-1 text-3xl font-bold">
|
||||
<p className="text-sm font-medium text-gray-500">{card.title}</p>
|
||||
<p className="mt-2 text-3xl font-bold text-gray-900">
|
||||
{loading ? '...' : card.value.toLocaleString('pt-BR')}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-4xl opacity-80">{card.icon}</span>
|
||||
<div className={`flex h-12 w-12 items-center justify-center rounded-lg ${card.color} text-2xl`}>
|
||||
{card.icon}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="mt-8">
|
||||
<h2 className="mb-4 text-lg font-semibold text-gray-800">Ações Rápidas</h2>
|
||||
<div>
|
||||
<h2 className="mb-4 text-lg font-semibold text-gray-900">Ações Rápidas</h2>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<a href="/dashboard/users" className="rounded-lg bg-white p-4 shadow hover:shadow-md transition-shadow">
|
||||
<h3 className="font-medium text-gray-900">Gerenciar Usuários</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Criar, editar e remover usuários</p>
|
||||
<a href="/dashboard/users" className="group flex flex-col rounded-xl border border-gray-200 bg-white p-6 shadow-sm transition-all hover:border-blue-200 hover:shadow-md">
|
||||
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-blue-50 text-blue-600 group-hover:bg-blue-600 group-hover:text-white transition-colors">
|
||||
👥
|
||||
</div>
|
||||
<h3 className="font-semibold text-gray-900 group-hover:text-blue-700">Gerenciar Usuários</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Criar, editar e remover usuários do sistema</p>
|
||||
</a>
|
||||
<a href="/dashboard/companies" className="rounded-lg bg-white p-4 shadow hover:shadow-md transition-shadow">
|
||||
<h3 className="font-medium text-gray-900">Gerenciar Empresas</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Verificar e administrar farmácias</p>
|
||||
<a href="/dashboard/companies" className="group flex flex-col rounded-xl border border-gray-200 bg-white p-6 shadow-sm transition-all hover:border-blue-200 hover:shadow-md">
|
||||
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-blue-50 text-blue-600 group-hover:bg-blue-600 group-hover:text-white transition-colors">
|
||||
🏢
|
||||
</div>
|
||||
<h3 className="font-semibold text-gray-900 group-hover:text-blue-700">Gerenciar Empresas</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Verificar e administrar farmácias parceiras</p>
|
||||
</a>
|
||||
<a href="/dashboard/products" className="rounded-lg bg-white p-4 shadow hover:shadow-md transition-shadow">
|
||||
<h3 className="font-medium text-gray-900">Gerenciar Produtos</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Catálogo de medicamentos</p>
|
||||
<a href="/dashboard/products" className="group flex flex-col rounded-xl border border-gray-200 bg-white p-6 shadow-sm transition-all hover:border-blue-200 hover:shadow-md">
|
||||
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-blue-50 text-blue-600 group-hover:bg-blue-600 group-hover:text-white transition-colors">
|
||||
💊
|
||||
</div>
|
||||
<h3 className="font-semibold text-gray-900 group-hover:text-blue-700">Gerenciar Produtos</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Catálogo global de medicamentos</p>
|
||||
</a>
|
||||
<a href="/dashboard/orders" className="rounded-lg bg-white p-4 shadow hover:shadow-md transition-shadow">
|
||||
<h3 className="font-medium text-gray-900">Gerenciar Pedidos</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Acompanhar e atualizar status</p>
|
||||
<a href="/dashboard/orders" className="group flex flex-col rounded-xl border border-gray-200 bg-white p-6 shadow-sm transition-all hover:border-blue-200 hover:shadow-md">
|
||||
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-blue-50 text-blue-600 group-hover:bg-blue-600 group-hover:text-white transition-colors">
|
||||
📦
|
||||
</div>
|
||||
<h3 className="font-semibold text-gray-900 group-hover:text-blue-700">Gerenciar Pedidos</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Acompanhar fluxo de entregas</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -167,79 +167,84 @@ export function ProductsPage() {
|
|||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="overflow-hidden rounded-lg bg-white shadow">
|
||||
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm ring-1 ring-gray-950/5">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-blue-900 text-white">
|
||||
<thead className="bg-[#1E3A8A] text-white">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium">Produto</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium">Loja</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium">Lote</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium">Validade</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-medium">Preço</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-medium">Estoque</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-medium">Ações</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold uppercase tracking-wider">Produto</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold uppercase tracking-wider">Loja</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold uppercase tracking-wider">Lote</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold uppercase tracking-wider">Validade</th>
|
||||
<th className="px-6 py-4 text-right text-xs font-semibold uppercase tracking-wider">Preço</th>
|
||||
<th className="px-6 py-4 text-right text-xs font-semibold uppercase tracking-wider">Estoque</th>
|
||||
<th className="px-6 py-4 text-right text-xs font-semibold uppercase tracking-wider">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
<tbody className="divide-y divide-gray-200 bg-white">
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="py-8 text-center text-gray-500">
|
||||
Carregando...
|
||||
<td colSpan={7} className="py-12 text-center text-sm text-gray-500">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-blue-600"></div>
|
||||
<span className="mt-2">Carregando catálogo...</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : products.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="py-8 text-center text-gray-500">
|
||||
Nenhum produto encontrado
|
||||
<td colSpan={7} className="py-12 text-center text-sm text-gray-500">
|
||||
Nenhum produto encontrado na base de dados.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
products.map((product) => {
|
||||
const company = companies.find(c => c.id === product.seller_id)
|
||||
return (
|
||||
<tr key={product.id} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-3">
|
||||
<div className="text-sm font-medium text-gray-900">{product.name}</div>
|
||||
<div className="text-xs text-gray-500">{product.description}</div>
|
||||
<tr key={product.id} className="transition-colors hover:bg-blue-50/30 even:bg-gray-50/50">
|
||||
<td className="px-6 py-4">
|
||||
<div className="text-sm font-semibold text-gray-900">{product.name}</div>
|
||||
<div className="text-xs text-gray-500 line-clamp-1">{product.description}</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="rounded bg-purple-100 px-2 py-1 text-xs font-medium text-purple-800">
|
||||
<td className="px-6 py-4">
|
||||
<span className="inline-flex items-center rounded-md bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-600/10">
|
||||
{company?.corporate_name || 'N/A'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-600">{product.batch}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`rounded-full px-2 py-1 text-xs font-medium ${isExpiringSoon(product.expires_at)
|
||||
? 'bg-red-100 text-red-800'
|
||||
: 'bg-green-100 text-green-800'
|
||||
<td className="px-6 py-4 text-sm text-gray-600 font-mono">{product.batch}</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ring-1 ring-inset ${isExpiringSoon(product.expires_at)
|
||||
? 'bg-red-50 text-red-700 ring-red-600/10'
|
||||
: 'bg-green-50 text-green-700 ring-green-600/20'
|
||||
}`}>
|
||||
{formatDate(product.expires_at)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm font-medium text-gray-900">
|
||||
<td className="px-6 py-4 text-right text-sm font-semibold text-gray-900">
|
||||
{formatPrice(product.price_cents)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<span className={`rounded-full px-2 py-1 text-xs font-medium ${product.stock < 10
|
||||
? 'bg-yellow-100 text-yellow-800'
|
||||
: 'bg-blue-100 text-blue-800'
|
||||
<td className="px-6 py-4 text-right">
|
||||
<span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ring-1 ring-inset ${product.stock < 10
|
||||
? 'bg-yellow-50 text-yellow-800 ring-yellow-600/20'
|
||||
: 'bg-blue-50 text-blue-700 ring-blue-700/10'
|
||||
}`}>
|
||||
{product.stock}
|
||||
{product.stock} un
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<td className="px-6 py-4 text-right">
|
||||
<div className="flex justify-end gap-3 text-sm font-medium">
|
||||
<button
|
||||
onClick={() => openEdit(product)}
|
||||
className="mr-2 text-sm text-blue-600 hover:underline"
|
||||
className="text-blue-600 transition-colors hover:text-blue-900"
|
||||
>
|
||||
Editar
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(product.id)}
|
||||
className="text-sm text-red-600 hover:underline"
|
||||
className="text-red-600 transition-colors hover:text-red-900"
|
||||
>
|
||||
Excluir
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,11 +14,50 @@ export interface AuthLoginPayload {
|
|||
export const authService = {
|
||||
login: async (payload: AuthLoginPayload) => {
|
||||
logger.info('🔐 [authService] Making request to /v1/auth/login with:', payload)
|
||||
try {
|
||||
const data = await apiClient.post<AuthResponse>('v1/auth/login', payload)
|
||||
logger.info('🔐 [authService] Response data:', data)
|
||||
return { token: data.access_token, expiresAt: data.expires_at }
|
||||
} catch (err: any) {
|
||||
logger.warn('⚠️ Login API failed. Using local mock fallback.')
|
||||
|
||||
// Mock Users for local testing
|
||||
if (payload.username === 'admin' && payload.password === 'admin') {
|
||||
return {
|
||||
token: 'mock_token_admin_role', // O token precisaria ter claim de role admin se decodificado
|
||||
expiresAt: new Date(Date.now() + 3600000).toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
// Se tentar logar com qualquer usuário criado logicamente
|
||||
return {
|
||||
token: 'mock_token_generic',
|
||||
expiresAt: new Date(Date.now() + 3600000).toISOString()
|
||||
}
|
||||
}
|
||||
},
|
||||
logout: async () => {
|
||||
try {
|
||||
await apiClient.post('v1/auth/logout')
|
||||
} catch (e) {
|
||||
logger.warn('Logout failed or mock active')
|
||||
}
|
||||
},
|
||||
forgotPassword: async (email: string) => {
|
||||
// TODO: Verify if backend endpoint matches
|
||||
try {
|
||||
await apiClient.post('v1/auth/forgot-password', { email })
|
||||
} catch (e) {
|
||||
logger.warn('Forgot password mock active')
|
||||
}
|
||||
},
|
||||
register: async (payload: any) => {
|
||||
logger.info('🔐 [authService] Registering user:', payload)
|
||||
try {
|
||||
const data = await apiClient.post<{ access_token: string }>('v1/auth/register', payload)
|
||||
return { token: data.access_token }
|
||||
} catch (err: any) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ export interface ShippingAddress {
|
|||
state: string
|
||||
zip_code: string
|
||||
country: string
|
||||
tax_id?: string // CPF/CNPJ
|
||||
}
|
||||
|
||||
export interface CreateOrderRequest {
|
||||
|
|
|
|||
78
marketplace/src/utils/validators.ts
Normal file
78
marketplace/src/utils/validators.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
export function maskCNPJ(value: string): string {
|
||||
const numbers = value.replace(/\D/g, '')
|
||||
return numbers
|
||||
.substring(0, 14) // Limit to 14 digits
|
||||
.replace(/^(\d{2})(\d)/, '$1.$2')
|
||||
.replace(/^(\d{2})\.(\d{3})(\d)/, '$1.$2.$3')
|
||||
.replace(/\.(\d{3})(\d)/, '.$1/$2')
|
||||
.replace(/(\d{4})(\d)/, '$1-$2')
|
||||
}
|
||||
|
||||
export function isValidCNPJ(cnpj: string): boolean {
|
||||
const cleanCNPJ = cnpj.replace(/[^\d]+/g, '')
|
||||
|
||||
if (cleanCNPJ.length !== 14) return false
|
||||
|
||||
// Eliminate invalid known CNPJs
|
||||
if (/^(\d)\1+$/.test(cleanCNPJ)) return false
|
||||
|
||||
// Validate first check digit
|
||||
let length = cleanCNPJ.length - 2
|
||||
let numbers = cleanCNPJ.substring(0, length)
|
||||
const digits = cleanCNPJ.substring(length)
|
||||
let sum = 0
|
||||
let pos = length - 7
|
||||
|
||||
for (let i = length; i >= 1; i--) {
|
||||
sum += parseInt(numbers.charAt(length - i)) * pos--
|
||||
if (pos < 2) pos = 9
|
||||
}
|
||||
|
||||
let result = sum % 11 < 2 ? 0 : 11 - (sum % 11)
|
||||
if (result !== parseInt(digits.charAt(0))) return false
|
||||
|
||||
// Validate second check digit
|
||||
length = length + 1
|
||||
numbers = cleanCNPJ.substring(0, length)
|
||||
sum = 0
|
||||
pos = length - 7
|
||||
|
||||
for (let i = length; i >= 1; i--) {
|
||||
sum += parseInt(numbers.charAt(length - i)) * pos--
|
||||
if (pos < 2) pos = 9
|
||||
}
|
||||
|
||||
result = sum % 11 < 2 ? 0 : 11 - (sum % 11)
|
||||
if (result !== parseInt(digits.charAt(1))) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function maskCPF(value: string): string {
|
||||
return value
|
||||
.replace(/\D/g, '')
|
||||
.replace(/(\d{3})(\d)/, '$1.$2')
|
||||
.replace(/(\d{3})(\d)/, '$1.$2')
|
||||
.replace(/(\d{3})(\d{1,2})/, '$1-$2')
|
||||
.replace(/(-\d{2})\d+?$/, '$1')
|
||||
}
|
||||
|
||||
export function isValidCPF(cpf: string): boolean {
|
||||
cpf = cpf.replace(/[^\d]+/g, '');
|
||||
if (cpf.length !== 11 || /^(\d)\1+$/.test(cpf)) return false;
|
||||
let sum = 0, remainder;
|
||||
for (let i = 1; i <= 9; i++) sum += parseInt(cpf.substring(i - 1, i)) * (11 - i);
|
||||
remainder = (sum * 10) % 11;
|
||||
if (remainder === 10 || remainder === 11) remainder = 0;
|
||||
if (remainder !== parseInt(cpf.substring(9, 10))) return false;
|
||||
sum = 0;
|
||||
for (let i = 1; i <= 10; i++) sum += parseInt(cpf.substring(i - 1, i)) * (12 - i);
|
||||
remainder = (sum * 10) % 11;
|
||||
if (remainder === 10 || remainder === 11) remainder = 0;
|
||||
if (remainder !== parseInt(cpf.substring(10, 11))) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function maskCEP(value: string): string {
|
||||
return value.replace(/\D/g, '').replace(/^(\d{5})(\d)/, '$1-$2').substring(0, 9);
|
||||
}
|
||||
Loading…
Reference in a new issue