Minimax correcao
This commit is contained in:
245
package-lock.json
generated
245
package-lock.json
generated
@@ -24,7 +24,6 @@
|
|||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jose": "^5.2.0",
|
"jose": "^5.2.0",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"mongodb": "^7.1.1",
|
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
@@ -56,7 +55,6 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
"mongoose": "^8.23.0",
|
|
||||||
"nodemon": "^3.1.11",
|
"nodemon": "^3.1.11",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
@@ -2391,13 +2389,6 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mongodb-js/saslprep": {
|
|
||||||
"version": "1.4.6",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"sparse-bitfield": "^3.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -3604,17 +3595,6 @@
|
|||||||
"version": "10.0.0",
|
"version": "10.0.0",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/webidl-conversions": {
|
|
||||||
"version": "7.0.3",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/whatwg-url": {
|
|
||||||
"version": "13.0.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/webidl-conversions": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/ws": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.18.1",
|
"version": "8.18.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -5042,13 +5022,6 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bson": {
|
|
||||||
"version": "7.2.0",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.19.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/buffer-from": {
|
"node_modules/buffer-from": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
@@ -8037,10 +8010,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/memory-pager": {
|
|
||||||
"version": "1.5.0",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/merge-descriptors": {
|
"node_modules/merge-descriptors": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -8164,179 +8133,6 @@
|
|||||||
"mkdirp": "bin/cmd.js"
|
"mkdirp": "bin/cmd.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mongodb": {
|
|
||||||
"version": "7.1.1",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@mongodb-js/saslprep": "^1.3.0",
|
|
||||||
"bson": "^7.1.1",
|
|
||||||
"mongodb-connection-string-url": "^7.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.19.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@aws-sdk/credential-providers": "^3.806.0",
|
|
||||||
"@mongodb-js/zstd": "^7.0.0",
|
|
||||||
"gcp-metadata": "^7.0.1",
|
|
||||||
"kerberos": "^7.0.0",
|
|
||||||
"mongodb-client-encryption": ">=7.0.0 <7.1.0",
|
|
||||||
"snappy": "^7.3.2",
|
|
||||||
"socks": "^2.8.6"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@aws-sdk/credential-providers": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@mongodb-js/zstd": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"gcp-metadata": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"kerberos": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"mongodb-client-encryption": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"snappy": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"socks": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mongodb-connection-string-url": {
|
|
||||||
"version": "7.0.1",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/whatwg-url": "^13.0.0",
|
|
||||||
"whatwg-url": "^14.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.19.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mongoose": {
|
|
||||||
"version": "8.23.0",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"bson": "^6.10.4",
|
|
||||||
"kareem": "2.6.3",
|
|
||||||
"mongodb": "~6.20.0",
|
|
||||||
"mpath": "0.9.0",
|
|
||||||
"mquery": "5.0.0",
|
|
||||||
"ms": "2.1.3",
|
|
||||||
"sift": "17.1.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16.20.1"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/mongoose"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mongoose/node_modules/@types/whatwg-url": {
|
|
||||||
"version": "11.0.5",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/webidl-conversions": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mongoose/node_modules/bson": {
|
|
||||||
"version": "6.10.4",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16.20.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mongoose/node_modules/kareem": {
|
|
||||||
"version": "2.6.3",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mongoose/node_modules/mongodb": {
|
|
||||||
"version": "6.20.0",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@mongodb-js/saslprep": "^1.3.0",
|
|
||||||
"bson": "^6.10.4",
|
|
||||||
"mongodb-connection-string-url": "^3.0.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16.20.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@aws-sdk/credential-providers": "^3.188.0",
|
|
||||||
"@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
|
|
||||||
"gcp-metadata": "^5.2.0",
|
|
||||||
"kerberos": "^2.0.1",
|
|
||||||
"mongodb-client-encryption": ">=6.0.0 <7",
|
|
||||||
"snappy": "^7.3.2",
|
|
||||||
"socks": "^2.7.1"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@aws-sdk/credential-providers": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@mongodb-js/zstd": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"gcp-metadata": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"kerberos": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"mongodb-client-encryption": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"snappy": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"socks": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mongoose/node_modules/mongodb-connection-string-url": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/whatwg-url": "^11.0.2",
|
|
||||||
"whatwg-url": "^14.1.0 || ^13.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mongoose/node_modules/mquery": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"debug": "4.x"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mpath": {
|
|
||||||
"version": "0.9.0",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mri": {
|
"node_modules/mri": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -8973,6 +8769,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -9791,11 +9588,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sift": {
|
|
||||||
"version": "17.1.3",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/signal-exit": {
|
"node_modules/signal-exit": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
@@ -9903,13 +9695,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/sparse-bitfield": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"memory-pager": "^1.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -10299,16 +10084,6 @@
|
|||||||
"nodetouch": "bin/nodetouch.js"
|
"nodetouch": "bin/nodetouch.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tr46": {
|
|
||||||
"version": "5.1.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"punycode": "^2.3.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tree-kill": {
|
"node_modules/tree-kill": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -11351,24 +11126,6 @@
|
|||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/webidl-conversions": {
|
|
||||||
"version": "7.0.0",
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/whatwg-url": {
|
|
||||||
"version": "14.2.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"tr46": "^5.1.0",
|
|
||||||
"webidl-conversions": "^7.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jose": "^5.2.0",
|
"jose": "^5.2.0",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"mongodb": "^7.1.1",
|
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
@@ -65,7 +64,6 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
"mongoose": "^8.23.0",
|
|
||||||
"nodemon": "^3.1.11",
|
"nodemon": "^3.1.11",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ import { DeveloperDashboard } from './pages/DeveloperDashboard';
|
|||||||
import { CalculatorDashboard } from './pages/CalculatorDashboard';
|
import { CalculatorDashboard } from './pages/CalculatorDashboard';
|
||||||
import { StockDashboard } from './pages/StockDashboard';
|
import { StockDashboard } from './pages/StockDashboard';
|
||||||
import { GuestDashboard } from './pages/GuestDashboard';
|
import { GuestDashboard } from './pages/GuestDashboard';
|
||||||
import { Login } from './pages/Login';
|
|
||||||
import { Callback } from './pages/Callback';
|
|
||||||
import InstrumentList from './pages/InstrumentList';
|
import InstrumentList from './pages/InstrumentList';
|
||||||
|
|
||||||
const DeveloperRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
const DeveloperRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
@@ -39,8 +37,6 @@ const AppContent: React.FC = () => {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<ProjectList />} />
|
<Route path="/" element={<ProjectList />} />
|
||||||
<Route path="/login" element={<Login />} />
|
|
||||||
<Route path="/callback" element={<Callback />} />
|
|
||||||
<Route path="/guest-dashboard" element={<GuestDashboard />} />
|
<Route path="/guest-dashboard" element={<GuestDashboard />} />
|
||||||
<Route path="/projects" element={<ProjectList />} />
|
<Route path="/projects" element={<ProjectList />} />
|
||||||
<Route path="/project/:id" element={<ProjectDetails />} />
|
<Route path="/project/:id" element={<ProjectDetails />} />
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import React, { useState } from 'react';
|
|||||||
import NotificationBell from './NotificationBell';
|
import NotificationBell from './NotificationBell';
|
||||||
import { TeamPresence } from './TeamPresence';
|
import { TeamPresence } from './TeamPresence';
|
||||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { Menu, X, FolderOpen, Layers, ClipboardCheck, LogOut, TrendingUp, Sun, Moon, HelpCircle, Shield, Wrench, Terminal, LayoutDashboard, Package, Thermometer, User } from 'lucide-react';
|
import { Menu, X, FolderOpen, Layers, ClipboardCheck, TrendingUp, Sun, Moon, HelpCircle, Shield, Wrench, Terminal, LayoutDashboard, Package, Thermometer, User } from 'lucide-react';
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
import { useLogto } from '@logto/react';
|
|
||||||
import { TechnicalManual } from './TechnicalManual';
|
import { TechnicalManual } from './TechnicalManual';
|
||||||
import { useAuth } from '../context/useAuth';
|
import { useAuth } from '../context/useAuth';
|
||||||
|
|
||||||
@@ -20,8 +19,7 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
return saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
return saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
});
|
});
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { signOut } = useLogto();
|
const { isAdmin, isUser, isDeveloper, appUser } = useAuth();
|
||||||
const { isAdmin, isUser, isDeveloper, appUser, isSignedIn } = useAuth();
|
|
||||||
|
|
||||||
// Helper to get role display name
|
// Helper to get role display name
|
||||||
const getRoleDisplay = () => {
|
const getRoleDisplay = () => {
|
||||||
@@ -54,14 +52,6 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
}
|
}
|
||||||
}, [appUser, location.pathname, navigate]);
|
}, [appUser, location.pathname, navigate]);
|
||||||
|
|
||||||
// Redirect to login if not signed in (except for login and callback pages)
|
|
||||||
React.useEffect(() => {
|
|
||||||
const publicPaths = ['/login', '/callback'];
|
|
||||||
if (!isSignedIn && !publicPaths.includes(location.pathname)) {
|
|
||||||
navigate('/login');
|
|
||||||
}
|
|
||||||
}, [isSignedIn, location.pathname, navigate]);
|
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
icon: React.ElementType;
|
icon: React.ElementType;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -87,10 +77,6 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
|
||||||
signOut(window.location.origin);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (location.pathname === '/login' || location.pathname === '/callback') {
|
if (location.pathname === '/login' || location.pathname === '/callback') {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
@@ -216,14 +202,6 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleLogout}
|
|
||||||
className="flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold text-text-secondary hover:text-error hover:bg-error/5 transition-all w-full"
|
|
||||||
>
|
|
||||||
<LogOut size={18} />
|
|
||||||
Sair
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="pt-2 flex items-center justify-between px-2">
|
<div className="pt-2 flex items-center justify-between px-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<NotificationBell />
|
<NotificationBell />
|
||||||
@@ -348,13 +326,6 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
{isDarkMode ? <Sun size={20} className="text-yellow-500" /> : <Moon size={20} className="text-primary" />}
|
{isDarkMode ? <Sun size={20} className="text-yellow-500" /> : <Moon size={20} className="text-primary" />}
|
||||||
{isDarkMode ? 'Modo Claro' : 'Modo Escuro'}
|
{isDarkMode ? 'Modo Claro' : 'Modo Escuro'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
onClick={handleLogout}
|
|
||||||
className="flex items-center gap-4 w-full text-text-main font-bold"
|
|
||||||
>
|
|
||||||
<LogOut size={20} />
|
|
||||||
Sair
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -23,21 +23,7 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
|||||||
requireEdit = false,
|
requireEdit = false,
|
||||||
redirectTo = '/',
|
redirectTo = '/',
|
||||||
}) => {
|
}) => {
|
||||||
const { appUser, isLoading, canEdit, isSignedIn } = useAuth();
|
const { appUser, canEdit } = useAuth();
|
||||||
|
|
||||||
// Show loading state
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center min-h-[60vh]">
|
|
||||||
<RefreshCw size={32} className="animate-spin text-primary" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check authentication
|
|
||||||
if (!isSignedIn) {
|
|
||||||
return <Navigate to="/login" replace />;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check role-based access
|
// Check role-based access
|
||||||
if (allowedRoles && appUser && !allowedRoles.includes(appUser.role)) {
|
if (allowedRoles && appUser && !allowedRoles.includes(appUser.role)) {
|
||||||
|
|||||||
@@ -1,120 +1,53 @@
|
|||||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { useLogto } from '@logto/react';
|
import type { AppUser, UserRole } from '../types';
|
||||||
import type { AppUser } from '../types';
|
|
||||||
import { AuthContext } from './AuthContextType';
|
import { AuthContext } from './AuthContextType';
|
||||||
import { setUser } from '../main';
|
import { getUser } from '../main';
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL || '/api';
|
|
||||||
|
|
||||||
interface AuthProviderProps {
|
interface AuthProviderProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultUser: AppUser = {
|
||||||
|
id: 'guest-user',
|
||||||
|
email: 'guest@gpi.app',
|
||||||
|
name: 'Guest User',
|
||||||
|
role: 'user',
|
||||||
|
isBanned: false,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||||
const { isAuthenticated, getAccessToken, fetchUserInfo, isLoading: isLogtoLoading } = useLogto();
|
|
||||||
const [appUser, setAppUser] = useState<AppUser | null>(null);
|
const [appUser, setAppUser] = useState<AppUser | null>(null);
|
||||||
const [isAppLoading, setIsAppLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const syncUser = useCallback(async () => {
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
setAppUser(null);
|
|
||||||
setIsAppLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIsAppLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
const token = await getAccessToken();
|
|
||||||
if (!token) throw new Error('Token não disponível');
|
|
||||||
|
|
||||||
// Busca dados básicos do Logto se necessário
|
|
||||||
const logtoUserInfo = await fetchUserInfo();
|
|
||||||
|
|
||||||
const response = await fetch(`${API_URL}/users/me`, {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status === 404 && logtoUserInfo) {
|
|
||||||
// Usuário não existe no banco (provavelmente redirecionamento pós-login)
|
|
||||||
// Vamos tentar sincronizar/provisionar
|
|
||||||
const syncResp = await fetch(`${API_URL}/users/sync`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: logtoUserInfo.email,
|
|
||||||
name: logtoUserInfo.name || logtoUserInfo.username || 'Usuário Logto',
|
|
||||||
logto_id: logtoUserInfo.sub
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!syncResp.ok) throw new Error('Falha ao sincronizar usuário');
|
|
||||||
|
|
||||||
// Tenta buscar novamente após o sync
|
|
||||||
return syncUser();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Falha ao carregar usuário');
|
|
||||||
}
|
|
||||||
|
|
||||||
const userData = await response.json();
|
|
||||||
const effectiveRole = userData.role || 'guest';
|
|
||||||
|
|
||||||
const user = {
|
|
||||||
...userData,
|
|
||||||
id: userData._id || userData.id,
|
|
||||||
role: effectiveRole,
|
|
||||||
};
|
|
||||||
|
|
||||||
setUser(token, user);
|
|
||||||
setAppUser(user);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error loading user:', err);
|
|
||||||
setError('Erro ao carregar dados do usuário');
|
|
||||||
setAppUser(null);
|
|
||||||
} finally {
|
|
||||||
setIsAppLoading(false);
|
|
||||||
}
|
|
||||||
}, [isAuthenticated, getAccessToken, fetchUserInfo]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLogtoLoading) {
|
const storedUser = getUser();
|
||||||
syncUser();
|
if (storedUser) {
|
||||||
|
setAppUser({ ...defaultUser, ...storedUser, role: storedUser.role as UserRole });
|
||||||
|
} else {
|
||||||
|
setAppUser(defaultUser);
|
||||||
}
|
}
|
||||||
}, [isLogtoLoading, syncUser]);
|
}, []);
|
||||||
|
|
||||||
const refetchUser = useCallback(async () => {
|
const isDeveloper = useCallback(() => false, []);
|
||||||
await syncUser();
|
const isAdmin = useCallback(() => true, []);
|
||||||
}, [syncUser]);
|
const isUser = useCallback(() => true, []);
|
||||||
|
const isGuest = useCallback(() => false, []);
|
||||||
const isDeveloper = useCallback(() => {
|
const canEdit = useCallback(() => true, []);
|
||||||
return appUser?.email === 'admtracksteel@gmail.com';
|
const refetchUser = useCallback(async () => {}, []);
|
||||||
}, [appUser]);
|
|
||||||
|
|
||||||
const isAdmin = useCallback(() => appUser?.role === 'admin' || isDeveloper(), [appUser, isDeveloper]);
|
|
||||||
const isUser = useCallback(() => appUser?.role === 'user' || isAdmin(), [appUser, isAdmin]);
|
|
||||||
const isGuest = useCallback(() => appUser?.role === 'guest' && !isDeveloper(), [appUser, isDeveloper]);
|
|
||||||
const canEdit = useCallback(() => (appUser?.role !== 'guest' && appUser?.role !== undefined) || isDeveloper(), [appUser, isDeveloper]);
|
|
||||||
|
|
||||||
const isLoading = isLogtoLoading || isAppLoading;
|
|
||||||
|
|
||||||
const value = useMemo(() => ({
|
const value = useMemo(() => ({
|
||||||
appUser,
|
appUser,
|
||||||
isLoading,
|
isLoading: false,
|
||||||
isSignedIn: isAuthenticated,
|
isSignedIn: true,
|
||||||
error,
|
error: null,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isUser,
|
isUser,
|
||||||
isGuest,
|
isGuest,
|
||||||
isDeveloper,
|
isDeveloper,
|
||||||
canEdit,
|
canEdit,
|
||||||
refetchUser,
|
refetchUser,
|
||||||
}), [appUser, isLoading, isAuthenticated, error, isAdmin, isUser, isGuest, isDeveloper, canEdit, refetchUser]);
|
}), [appUser, isAdmin, isUser, isGuest, isDeveloper, canEdit, refetchUser]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={value}>
|
<AuthContext.Provider value={value}>
|
||||||
|
|||||||
@@ -1,34 +1,22 @@
|
|||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import { LogtoProvider, type LogtoConfig } from '@logto/react';
|
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
|
||||||
const LOGTO_URL = import.meta.env.VITE_LOGTO_URL || 'https://logto-admin-bzlued1boxl3t8ewsyn99an9.187.77.227.172.sslip.io';
|
|
||||||
const APP_ID = import.meta.env.VITE_LOGTO_APP_ID || 'gpi-app-final';
|
|
||||||
|
|
||||||
const config: LogtoConfig = {
|
|
||||||
endpoint: LOGTO_URL,
|
|
||||||
appId: APP_ID,
|
|
||||||
scopes: ['openid', 'profile', 'email'],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mantenha estas para compatibilidade temporária se necessário
|
|
||||||
export function getToken() {
|
export function getToken() {
|
||||||
return sessionStorage.getItem('logto_token');
|
return 'guest-token';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUser() {
|
export function getUser() {
|
||||||
const user = sessionStorage.getItem('logto_user');
|
return {
|
||||||
return user ? JSON.parse(user) : null;
|
id: 'guest-user',
|
||||||
|
email: 'guest@gpi.app',
|
||||||
|
name: 'Guest User',
|
||||||
|
role: 'user'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setUser(token: string, user: any) {
|
export function setUser(token: string, user: any) {
|
||||||
sessionStorage.setItem('logto_token', token);
|
console.log('User set (no auth):', user);
|
||||||
sessionStorage.setItem('logto_user', JSON.stringify(user));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(<App />)
|
||||||
<LogtoProvider config={config}>
|
|
||||||
<App />
|
|
||||||
</LogtoProvider>
|
|
||||||
)
|
|
||||||
@@ -5,7 +5,7 @@ import '../middleware/authMiddleware.js'; // Ensure type augmentation
|
|||||||
export const createApplicationRecord = async (req: Request, res: Response) => {
|
export const createApplicationRecord = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const createdBy = req.appUser?.clerkId;
|
const createdBy = req.appUser?.email || 'guest';
|
||||||
const record = await appRecordService.createApplicationRecord({ ...req.body, organizationId, createdBy });
|
const record = await appRecordService.createApplicationRecord({ ...req.body, organizationId, createdBy });
|
||||||
res.status(201).json(record);
|
res.status(201).json(record);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
@@ -29,17 +29,10 @@ export const getApplicationRecordsByProject = async (req: Request, res: Response
|
|||||||
export const updateApplicationRecord = async (req: Request, res: Response) => {
|
export const updateApplicationRecord = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const userId = req.appUser?.clerkId;
|
|
||||||
const userRole = req.appUser?.organizationRole || req.appUser?.role;
|
|
||||||
const isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com';
|
|
||||||
|
|
||||||
const record = await appRecordService.updateApplicationRecord(
|
const record = await appRecordService.updateApplicationRecord(
|
||||||
req.params.id as string,
|
req.params.id as string,
|
||||||
req.body,
|
req.body
|
||||||
organizationId,
|
|
||||||
userId,
|
|
||||||
userRole as any,
|
|
||||||
isDeveloper
|
|
||||||
);
|
);
|
||||||
if (!record) return res.status(403).json({ error: 'Não autorizado ou não encontrado.' });
|
if (!record) return res.status(403).json({ error: 'Não autorizado ou não encontrado.' });
|
||||||
res.json(record);
|
res.json(record);
|
||||||
@@ -51,17 +44,8 @@ export const updateApplicationRecord = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
export const deleteApplicationRecord = async (req: Request, res: Response) => {
|
export const deleteApplicationRecord = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
|
||||||
const userId = req.appUser?.clerkId;
|
|
||||||
const userRole = req.appUser?.organizationRole || req.appUser?.role;
|
|
||||||
const isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com';
|
|
||||||
|
|
||||||
const success = await appRecordService.deleteApplicationRecord(
|
const success = await appRecordService.deleteApplicationRecord(
|
||||||
req.params.id as string,
|
req.params.id as string
|
||||||
organizationId,
|
|
||||||
userId,
|
|
||||||
userRole as any,
|
|
||||||
isDeveloper
|
|
||||||
);
|
);
|
||||||
if (!success) return res.status(403).json({ error: 'Não autorizado ou não encontrado.' });
|
if (!success) return res.status(403).json({ error: 'Não autorizado ou não encontrado.' });
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import '../middleware/authMiddleware.js'; // Ensure type augmentation
|
|||||||
export const createInspection = async (req: Request, res: Response) => {
|
export const createInspection = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const createdBy = req.appUser?.clerkId;
|
const createdBy = req.appUser?.email || 'guest';
|
||||||
const inspection = await inspectionService.createInspection({
|
const inspection = await inspectionService.createInspection({
|
||||||
...req.body,
|
...req.body,
|
||||||
organizationId,
|
organizationId,
|
||||||
@@ -46,17 +46,10 @@ export const getInspectionsByProject = async (req: Request, res: Response) => {
|
|||||||
export const updateInspection = async (req: Request, res: Response) => {
|
export const updateInspection = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const userId = req.appUser?.clerkId;
|
|
||||||
const userRole = req.appUser?.organizationRole || req.appUser?.role;
|
|
||||||
const isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com';
|
|
||||||
|
|
||||||
const inspection = await inspectionService.updateInspection(
|
const inspection = await inspectionService.updateInspection(
|
||||||
req.params.id as string,
|
req.params.id as string,
|
||||||
req.body,
|
req.body
|
||||||
organizationId,
|
|
||||||
userId,
|
|
||||||
userRole as any,
|
|
||||||
isDeveloper
|
|
||||||
);
|
);
|
||||||
if (!inspection) return res.status(403).json({ error: 'Não autorizado ou não encontrado.' });
|
if (!inspection) return res.status(403).json({ error: 'Não autorizado ou não encontrado.' });
|
||||||
res.json(inspection);
|
res.json(inspection);
|
||||||
@@ -69,16 +62,9 @@ export const updateInspection = async (req: Request, res: Response) => {
|
|||||||
export const deleteInspection = async (req: Request, res: Response) => {
|
export const deleteInspection = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const userId = req.appUser?.clerkId;
|
|
||||||
const userRole = req.appUser?.organizationRole || req.appUser?.role;
|
|
||||||
const isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com';
|
|
||||||
|
|
||||||
const success = await inspectionService.deleteInspection(
|
const success = await inspectionService.deleteInspection(
|
||||||
req.params.id as string,
|
req.params.id as string
|
||||||
organizationId,
|
|
||||||
userId,
|
|
||||||
userRole as any,
|
|
||||||
isDeveloper
|
|
||||||
);
|
);
|
||||||
if (!success) return res.status(403).json({ error: 'Não autorizado ou não encontrado.' });
|
if (!success) return res.status(403).json({ error: 'Não autorizado ou não encontrado.' });
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
@@ -91,7 +77,9 @@ export const deleteInspection = async (req: Request, res: Response) => {
|
|||||||
export const getAllInspections = async (req: Request, res: Response) => {
|
export const getAllInspections = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const inspections = await inspectionService.getAllInspections(organizationId);
|
const inspections = organizationId
|
||||||
|
? await inspectionService.getInspectionsByOrganization(organizationId)
|
||||||
|
: await inspectionService.getInspectionStats();
|
||||||
res.json(inspections);
|
res.json(inspections);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { SystemSettings } from '../lib/compat.js';
|
import { SystemSettings } from '../lib/compat.js';
|
||||||
import { User, OrganizationMember, Organization } from '../lib/compat.js';
|
import { User, Organization, OrganizationMember } from '../lib/compat.js';
|
||||||
|
import { supabase } from '../config/supabase.js';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
@@ -29,16 +30,28 @@ export const updateSettings = async (req: Request, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const { appName, appSubtitle, appLogoUrl } = req.body;
|
const { appName, appSubtitle, appLogoUrl } = req.body;
|
||||||
|
|
||||||
const settings = await SystemSettings.findOneAndUpdate(
|
const existing = await SystemSettings.findOne({ settingsId: 'global' });
|
||||||
{ settingsId: 'global' },
|
|
||||||
{
|
let settings;
|
||||||
|
if (!existing) {
|
||||||
|
settings = await SystemSettings.create({
|
||||||
|
settingsId: 'global',
|
||||||
appName,
|
appName,
|
||||||
appSubtitle,
|
appSubtitle,
|
||||||
appLogoUrl,
|
appLogoUrl,
|
||||||
updatedBy: req.appUser?.email
|
updatedBy: req.appUser?.email
|
||||||
},
|
});
|
||||||
{ new: true, upsert: true } // Create if not exists
|
} else {
|
||||||
);
|
settings = await SystemSettings.findByIdAndUpdate(
|
||||||
|
existing.id,
|
||||||
|
{
|
||||||
|
appName,
|
||||||
|
appSubtitle,
|
||||||
|
appLogoUrl,
|
||||||
|
updatedBy: req.appUser?.email
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`⚙️ System Settings updated by ${req.appUser?.email}`);
|
console.log(`⚙️ System Settings updated by ${req.appUser?.email}`);
|
||||||
res.json(settings);
|
res.json(settings);
|
||||||
@@ -92,8 +105,13 @@ export const uploadLogo = async (req: Request, res: Response) => {
|
|||||||
// Global Admin Functions
|
// Global Admin Functions
|
||||||
export const getGlobalUsers = async (req: Request, res: Response) => {
|
export const getGlobalUsers = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const users = await User.find({}).sort({ createdAt: -1 });
|
const { data: users, error } = await supabase
|
||||||
res.json(users);
|
.from('users')
|
||||||
|
.select('*')
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
res.json(users || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting global users:', error);
|
console.error('Error getting global users:', error);
|
||||||
res.status(500).json({ error: 'Erro ao buscar usuários globais.' });
|
res.status(500).json({ error: 'Erro ao buscar usuários globais.' });
|
||||||
@@ -102,51 +120,28 @@ export const getGlobalUsers = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
export const getGlobalOrganizations = async (req: Request, res: Response) => {
|
export const getGlobalOrganizations = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
// Aggregate members to group by org and get full member lists
|
const { data: organizations, error } = await supabase
|
||||||
const organizations = await OrganizationMember.aggregate([
|
.from('organizations')
|
||||||
{
|
.select('*');
|
||||||
$group: {
|
|
||||||
_id: '$organizationId',
|
|
||||||
members: {
|
|
||||||
$push: {
|
|
||||||
name: '$name',
|
|
||||||
email: '$email',
|
|
||||||
role: '$role',
|
|
||||||
clerkUserId: '$clerkUserId',
|
|
||||||
isBanned: '$isBanned'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
lastActive: { $max: '$updatedAt' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$lookup: {
|
|
||||||
from: 'organizations', // Ensure this matches the collection name of Organization model
|
|
||||||
localField: '_id',
|
|
||||||
foreignField: 'clerkId',
|
|
||||||
as: 'orgDetails'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$unwind: {
|
|
||||||
path: '$orgDetails',
|
|
||||||
preserveNullAndEmptyArrays: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$project: {
|
|
||||||
_id: 1,
|
|
||||||
lastActive: 1,
|
|
||||||
members: 1,
|
|
||||||
memberCount: { $size: '$members' },
|
|
||||||
isBanned: { $ifNull: ['$orgDetails.isBanned', false] },
|
|
||||||
name: { $ifNull: ['$orgDetails.name', ''] }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ $sort: { memberCount: -1 } }
|
|
||||||
]);
|
|
||||||
|
|
||||||
res.json(organizations);
|
if (error) throw error;
|
||||||
|
|
||||||
|
const orgsWithMembers = await Promise.all(
|
||||||
|
(organizations || []).map(async (org) => {
|
||||||
|
const { data: members } = await supabase
|
||||||
|
.from('user_organizations')
|
||||||
|
.select('*')
|
||||||
|
.eq('organization_id', org.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...org,
|
||||||
|
members: members || [],
|
||||||
|
memberCount: members?.length || 0
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json(orgsWithMembers);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting global organizations:', error);
|
console.error('Error getting global organizations:', error);
|
||||||
res.status(500).json({ error: 'Erro ao buscar organizações globais.' });
|
res.status(500).json({ error: 'Erro ao buscar organizações globais.' });
|
||||||
@@ -161,12 +156,14 @@ export const toggleOrganizationBan = async (req: Request, res: Response) => {
|
|||||||
return res.status(400).json({ error: 'ID da organização é obrigatório.' });
|
return res.status(400).json({ error: 'ID da organização é obrigatório.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upsert the Organization record
|
const { data: org, error } = await supabase
|
||||||
const org = await Organization.findOneAndUpdate(
|
.from('organizations')
|
||||||
{ clerkId: organizationId },
|
.update({ is_banned: isBanned })
|
||||||
{ isBanned: isBanned },
|
.eq('id', organizationId)
|
||||||
{ new: true, upsert: true }
|
.select()
|
||||||
);
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
console.log(`Organization ${organizationId} ban status set to ${isBanned} by ${req.appUser?.email}`);
|
console.log(`Organization ${organizationId} ban status set to ${isBanned} by ${req.appUser?.email}`);
|
||||||
res.json(org);
|
res.json(org);
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { authenticateRequest } from './logtoAuth.js';
|
|
||||||
import { findOneGpi } from '../config/supabase.js';
|
|
||||||
|
|
||||||
export interface IAppUser {
|
export interface IAppUser {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -19,24 +17,16 @@ declare module 'express-serve-static-core' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const extractUser = async (req: Request, res: Response, next: NextFunction) => {
|
export const extractUser = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
req.appUser = {
|
||||||
const authHeader = req.headers.authorization;
|
id: 'guest-user',
|
||||||
|
logtoId: 'guest',
|
||||||
if (!authHeader?.startsWith('Bearer ')) {
|
email: 'guest@gpi.app',
|
||||||
return next();
|
name: 'Guest User',
|
||||||
}
|
role: 'user',
|
||||||
|
organizationId: 'default-org',
|
||||||
const user = await authenticateRequest(req);
|
organizationRole: 'user'
|
||||||
|
};
|
||||||
if (user) {
|
next();
|
||||||
req.appUser = user;
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error extracting user:', error);
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requireRole = (allowedRoles: string[]) => {
|
export const requireRole = (allowedRoles: string[]) => {
|
||||||
@@ -44,17 +34,6 @@ export const requireRole = (allowedRoles: string[]) => {
|
|||||||
if (!req.appUser) {
|
if (!req.appUser) {
|
||||||
return res.status(401).json({ error: 'Autenticação necessária.' });
|
return res.status(401).json({ error: 'Autenticação necessária.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.appUser.email === 'admtracksteel@gmail.com') {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
const effectiveRole = req.appUser.role;
|
|
||||||
|
|
||||||
if (!allowedRoles.includes(effectiveRole)) {
|
|
||||||
return res.status(403).json({ error: 'Acesso negado. Permissões insuficientes.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -63,26 +42,9 @@ export const requireAdmin = requireRole(['admin']);
|
|||||||
export const requireUser = requireRole(['user', 'admin']);
|
export const requireUser = requireRole(['user', 'admin']);
|
||||||
|
|
||||||
export const canEdit = (req: Request, res: Response, next: NextFunction) => {
|
export const canEdit = (req: Request, res: Response, next: NextFunction) => {
|
||||||
if (!req.appUser) {
|
|
||||||
return res.status(401).json({ error: 'Autenticação necessária.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.appUser.role === 'guest') {
|
|
||||||
return res.status(403).json({ error: 'Convidados não podem editar.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requireDeveloper = (req: Request, res: Response, next: NextFunction) => {
|
export const requireDeveloper = (req: Request, res: Response, next: NextFunction) => {
|
||||||
if (!req.appUser) {
|
|
||||||
return res.status(401).json({ error: 'Autenticação necessária.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.appUser.email !== 'admtracksteel@gmail.com') {
|
|
||||||
console.warn(`⛔ Attempted unauthorized developer access by: ${req.appUser.email}`);
|
|
||||||
return res.status(403).json({ error: 'Acesso restrito ao desenvolvedor.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export interface IApplicationRecord extends Document {
|
|
||||||
organizationId?: string;
|
|
||||||
createdBy?: string;
|
|
||||||
projectId: mongoose.Types.ObjectId;
|
|
||||||
coatStage: string;
|
|
||||||
pieceDescription?: string | null;
|
|
||||||
date?: Date | null;
|
|
||||||
operator?: string | null;
|
|
||||||
realWeight?: number | null;
|
|
||||||
volumeUsed?: number | null;
|
|
||||||
areaPainted?: number | null;
|
|
||||||
wetThicknessAvg?: number | null;
|
|
||||||
dryThicknessCalc?: number | null;
|
|
||||||
method?: string | null;
|
|
||||||
diluentUsed?: number | null;
|
|
||||||
notes?: string | null;
|
|
||||||
items?: {
|
|
||||||
partId: mongoose.Types.ObjectId;
|
|
||||||
quantity: number;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const ApplicationRecordSchema: Schema = new Schema({
|
|
||||||
organizationId: { type: String, index: true },
|
|
||||||
createdBy: { type: String, index: true },
|
|
||||||
projectId: { type: Schema.Types.ObjectId, ref: 'Project', required: true },
|
|
||||||
coatStage: { type: String, required: true },
|
|
||||||
pieceDescription: { type: String }, // Can be auto-generated or manual name for the Batch
|
|
||||||
date: { type: Date },
|
|
||||||
operator: { type: String },
|
|
||||||
realWeight: { type: Number },
|
|
||||||
volumeUsed: { type: Number },
|
|
||||||
areaPainted: { type: Number },
|
|
||||||
wetThicknessAvg: { type: Number },
|
|
||||||
dryThicknessCalc: { type: Number },
|
|
||||||
method: { type: String },
|
|
||||||
diluentUsed: { type: Number },
|
|
||||||
notes: { type: String },
|
|
||||||
items: [{
|
|
||||||
partId: { type: Schema.Types.ObjectId, ref: 'Part' },
|
|
||||||
quantity: { type: Number, required: true }
|
|
||||||
}]
|
|
||||||
}, { timestamps: true });
|
|
||||||
|
|
||||||
export default mongoose.models.ApplicationRecord || mongoose.model<IApplicationRecord>('ApplicationRecord', ApplicationRecordSchema);
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import mongoose, { Document, Schema } from 'mongoose';
|
|
||||||
|
|
||||||
export interface IGeometryType extends Document {
|
|
||||||
name: string;
|
|
||||||
efficiencyLoss: number; // Percentage, e.g., 10 for 10%
|
|
||||||
organizationId: string;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GeometryTypeSchema: Schema = new Schema({
|
|
||||||
name: { type: String, required: true },
|
|
||||||
efficiencyLoss: { type: Number, required: true, default: 0 },
|
|
||||||
organizationId: { type: String, required: true, index: true },
|
|
||||||
}, {
|
|
||||||
timestamps: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Compound index to ensure unique names per organization
|
|
||||||
GeometryTypeSchema.index({ organizationId: 1, name: 1 }, { unique: true });
|
|
||||||
|
|
||||||
export default mongoose.model<IGeometryType>('GeometryType', GeometryTypeSchema);
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export interface IInspection extends Document {
|
|
||||||
organizationId?: string;
|
|
||||||
createdBy?: string; // Clerk User ID
|
|
||||||
projectId: mongoose.Types.ObjectId;
|
|
||||||
type: 'painting' | 'surface_treatment';
|
|
||||||
|
|
||||||
// Common
|
|
||||||
date?: Date | null;
|
|
||||||
inspector?: string | null;
|
|
||||||
appearance?: 'approved' | 'rejected' | 'notes' | null; // Unified status
|
|
||||||
defects?: string | null; // Observations
|
|
||||||
photos?: string[]; // URLs
|
|
||||||
partTemperature?: number | null;
|
|
||||||
weightKg?: number | null;
|
|
||||||
|
|
||||||
// Painting Specific
|
|
||||||
pieceDescription?: string | null;
|
|
||||||
epsPoints?: (number | null)[];
|
|
||||||
adhesionTest?: string | null;
|
|
||||||
|
|
||||||
// Surface Treatment Specific
|
|
||||||
batch?: string | null; // Lote
|
|
||||||
treatmentExecutor?: string | null;
|
|
||||||
treatmentType?: string | null; // Jateamento, Mecânica...
|
|
||||||
cleaningDegree?: string | null; // Sa 2.5, St 3...
|
|
||||||
roughnessReadings?: (number | null)[]; // 5 measurements
|
|
||||||
flashRust?: string | null;
|
|
||||||
temperature?: number | null;
|
|
||||||
relativeHumidity?: number | null;
|
|
||||||
period?: 'morning' | 'afternoon' | 'night' | null;
|
|
||||||
applicationRecordId?: mongoose.Types.ObjectId; // Link to specific painting batch
|
|
||||||
stockItemId?: mongoose.Types.ObjectId; // Link to Stock Item (Paint used)
|
|
||||||
instrumentId?: mongoose.Types.ObjectId; // Link to Instrument used
|
|
||||||
}
|
|
||||||
|
|
||||||
const InspectionSchema: Schema = new Schema({
|
|
||||||
organizationId: { type: String, index: true },
|
|
||||||
createdBy: { type: String, index: true },
|
|
||||||
projectId: { type: Schema.Types.ObjectId, ref: 'Project', required: true },
|
|
||||||
applicationRecordId: { type: Schema.Types.ObjectId, ref: 'ApplicationRecord' },
|
|
||||||
stockItemId: { type: Schema.Types.ObjectId, ref: 'StockItem' },
|
|
||||||
instrumentId: { type: Schema.Types.ObjectId, ref: 'Instrument' },
|
|
||||||
type: { type: String, enum: ['painting', 'surface_treatment'], default: 'painting', index: true },
|
|
||||||
|
|
||||||
// Common
|
|
||||||
date: { type: Date },
|
|
||||||
inspector: { type: String },
|
|
||||||
appearance: { type: String }, // approved, rejected, notes
|
|
||||||
defects: { type: String },
|
|
||||||
photos: [{ type: String }],
|
|
||||||
partTemperature: { type: Number },
|
|
||||||
weightKg: { type: Number },
|
|
||||||
|
|
||||||
// Painting
|
|
||||||
pieceDescription: { type: String },
|
|
||||||
epsPoints: [{ type: Number }],
|
|
||||||
adhesionTest: { type: String },
|
|
||||||
|
|
||||||
// Surface Treatment
|
|
||||||
batch: { type: String },
|
|
||||||
treatmentExecutor: { type: String },
|
|
||||||
treatmentType: { type: String },
|
|
||||||
cleaningDegree: { type: String },
|
|
||||||
roughnessReadings: [{ type: Number }],
|
|
||||||
flashRust: { type: String },
|
|
||||||
temperature: { type: Number },
|
|
||||||
relativeHumidity: { type: Number },
|
|
||||||
period: { type: String },
|
|
||||||
}, { timestamps: true });
|
|
||||||
|
|
||||||
export default mongoose.models.Inspection || mongoose.model<IInspection>('Inspection', InspectionSchema);
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export interface IInstrument extends Document {
|
|
||||||
organizationId: string;
|
|
||||||
name: string;
|
|
||||||
type: string; // Ex: Medidor de Camada, Termo-higrômetro
|
|
||||||
manufacturer?: string;
|
|
||||||
modelName?: string;
|
|
||||||
serialNumber: string;
|
|
||||||
calibrationDate?: Date;
|
|
||||||
calibrationExpirationDate?: Date;
|
|
||||||
certificateUrl?: string; // URL do PDF
|
|
||||||
status: 'active' | 'inactive' | 'maintenance' | 'expired';
|
|
||||||
notes?: string;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const InstrumentSchema: Schema = new Schema({
|
|
||||||
organizationId: { type: String, required: true, index: true },
|
|
||||||
name: { type: String, required: true },
|
|
||||||
type: { type: String, required: true },
|
|
||||||
manufacturer: { type: String },
|
|
||||||
modelName: { type: String },
|
|
||||||
serialNumber: { type: String, required: true },
|
|
||||||
calibrationDate: { type: Date },
|
|
||||||
calibrationExpirationDate: { type: Date },
|
|
||||||
certificateUrl: { type: String },
|
|
||||||
status: {
|
|
||||||
type: String,
|
|
||||||
enum: ['active', 'inactive', 'maintenance', 'expired'],
|
|
||||||
default: 'active'
|
|
||||||
},
|
|
||||||
notes: { type: String }
|
|
||||||
}, { timestamps: true });
|
|
||||||
|
|
||||||
// Index para evitar duplicidade de número de série dentro da mesma organização
|
|
||||||
InstrumentSchema.index({ organizationId: 1, serialNumber: 1 }, { unique: true });
|
|
||||||
|
|
||||||
export default mongoose.models.Instrument || mongoose.model<IInstrument>('Instrument', InstrumentSchema);
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export interface IMessage extends Document {
|
|
||||||
organizationId: string;
|
|
||||||
fromUserId: string; // clerkId do remetente
|
|
||||||
toUserId: string; // clerkId do destinatário
|
|
||||||
message: string;
|
|
||||||
isRead: boolean;
|
|
||||||
readAt?: Date;
|
|
||||||
isArchived: boolean;
|
|
||||||
isDeletedByRecipient: boolean;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MessageSchema: Schema = new Schema(
|
|
||||||
{
|
|
||||||
organizationId: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
index: true,
|
|
||||||
},
|
|
||||||
fromUserId: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
index: true,
|
|
||||||
},
|
|
||||||
toUserId: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
index: true,
|
|
||||||
},
|
|
||||||
message: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
maxlength: 255,
|
|
||||||
},
|
|
||||||
isRead: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
readAt: {
|
|
||||||
type: Date,
|
|
||||||
},
|
|
||||||
isArchived: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
isDeletedByRecipient: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timestamps: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Compound index for efficient queries
|
|
||||||
MessageSchema.index({ toUserId: 1, isRead: 1 });
|
|
||||||
MessageSchema.index({ fromUserId: 1, toUserId: 1 });
|
|
||||||
|
|
||||||
export default mongoose.model<IMessage>('Message', MessageSchema);
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export type NotificationType = 'info' | 'warning' | 'error' | 'success';
|
|
||||||
|
|
||||||
export interface INotification extends Document {
|
|
||||||
organizationId: string;
|
|
||||||
recipientId?: string; // Se null, é para todos da organização
|
|
||||||
title: string;
|
|
||||||
message: string;
|
|
||||||
type: NotificationType;
|
|
||||||
isRead: boolean;
|
|
||||||
isArchived: boolean;
|
|
||||||
archivedBy: string[]; // IDs dos usuários que arquivaram (para notificações globais)
|
|
||||||
deletedBy: string[]; // IDs dos usuários que deletaram (para notificações globais)
|
|
||||||
metadata?: any; // Para guardar IDs de projetos, itens, etc.
|
|
||||||
createdAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NotificationSchema: Schema = new Schema({
|
|
||||||
organizationId: { type: String, required: true, index: true },
|
|
||||||
recipientId: { type: String, index: true }, // Opcional
|
|
||||||
title: { type: String, required: true },
|
|
||||||
message: { type: String, required: true },
|
|
||||||
type: { type: String, enum: ['info', 'warning', 'error', 'success'], default: 'info' },
|
|
||||||
isRead: { type: Boolean, default: false },
|
|
||||||
isArchived: { type: Boolean, default: false },
|
|
||||||
archivedBy: [{ type: String }],
|
|
||||||
deletedBy: [{ type: String }],
|
|
||||||
metadata: { type: Schema.Types.Mixed },
|
|
||||||
}, { timestamps: true });
|
|
||||||
|
|
||||||
export default mongoose.models.Notification || mongoose.model<INotification>('Notification', NotificationSchema);
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export interface IOrganization extends Document {
|
|
||||||
clerkId: string;
|
|
||||||
name?: string;
|
|
||||||
isBanned: boolean;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const OrganizationSchema: Schema = new Schema({
|
|
||||||
clerkId: { type: String, required: true, unique: true, index: true },
|
|
||||||
name: { type: String },
|
|
||||||
isBanned: { type: Boolean, default: false },
|
|
||||||
}, { timestamps: true });
|
|
||||||
|
|
||||||
export default mongoose.models.Organization || mongoose.model<IOrganization>('Organization', OrganizationSchema);
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export type OrgRole = 'guest' | 'user' | 'admin';
|
|
||||||
|
|
||||||
export interface IOrganizationMember extends Document {
|
|
||||||
clerkUserId: string;
|
|
||||||
organizationId: string;
|
|
||||||
role: OrgRole;
|
|
||||||
isBanned: boolean;
|
|
||||||
// Denormalized user info for quick access
|
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const OrganizationMemberSchema: Schema = new Schema({
|
|
||||||
clerkUserId: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
index: true
|
|
||||||
},
|
|
||||||
organizationId: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
index: true
|
|
||||||
},
|
|
||||||
role: {
|
|
||||||
type: String,
|
|
||||||
enum: ['guest', 'user', 'admin'],
|
|
||||||
default: 'guest'
|
|
||||||
},
|
|
||||||
isBanned: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
timestamps: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Compound index for unique user per organization
|
|
||||||
OrganizationMemberSchema.index({ clerkUserId: 1, organizationId: 1 }, { unique: true });
|
|
||||||
|
|
||||||
export default mongoose.models.OrganizationMember || mongoose.model<IOrganizationMember>('OrganizationMember', OrganizationMemberSchema);
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export interface IPaintingScheme extends Document {
|
|
||||||
projectId: mongoose.Types.ObjectId;
|
|
||||||
name: string;
|
|
||||||
type?: string | null;
|
|
||||||
coat?: string | null;
|
|
||||||
solidsVolume?: number | null;
|
|
||||||
yieldTheoretical?: number | null;
|
|
||||||
epsMin?: number | null;
|
|
||||||
epsMax?: number | null;
|
|
||||||
dilution?: number | null;
|
|
||||||
manufacturer?: string | null;
|
|
||||||
color?: string | null;
|
|
||||||
notes?: string | null;
|
|
||||||
organizationId?: string;
|
|
||||||
// Consumption Planning
|
|
||||||
paintConsumption?: number | null;
|
|
||||||
thinnerConsumption?: number | null;
|
|
||||||
paintId?: mongoose.Types.ObjectId | null; // Ref to TechnicalDataSheet
|
|
||||||
thinnerId?: mongoose.Types.ObjectId | null; // Ref to TechnicalDataSheet
|
|
||||||
preferredStockItemId?: mongoose.Types.ObjectId | null; // Ref to StockItem (Suggested Batch)
|
|
||||||
}
|
|
||||||
|
|
||||||
const PaintingSchemeSchema: Schema = new Schema({
|
|
||||||
organizationId: { type: String, index: true },
|
|
||||||
projectId: { type: Schema.Types.ObjectId, ref: 'Project', required: true },
|
|
||||||
name: { type: String, required: true },
|
|
||||||
type: { type: String },
|
|
||||||
coat: { type: String },
|
|
||||||
solidsVolume: { type: Number },
|
|
||||||
yieldTheoretical: { type: Number },
|
|
||||||
epsMin: { type: Number },
|
|
||||||
epsMax: { type: Number },
|
|
||||||
dilution: { type: Number },
|
|
||||||
manufacturer: { type: String },
|
|
||||||
color: { type: String },
|
|
||||||
notes: { type: String },
|
|
||||||
// Consumption Planning
|
|
||||||
paintConsumption: { type: Number },
|
|
||||||
thinnerConsumption: { type: Number },
|
|
||||||
paintId: { type: Schema.Types.ObjectId, ref: 'TechnicalDataSheet' },
|
|
||||||
thinnerId: { type: Schema.Types.ObjectId, ref: 'TechnicalDataSheet' },
|
|
||||||
preferredStockItemId: { type: Schema.Types.ObjectId, ref: 'StockItem' }
|
|
||||||
}, { strict: false });
|
|
||||||
|
|
||||||
console.log("✅✅✅ PAINTING SCHEME MODEL (WITH CONSUMPTION) LOADED ✅✅✅");
|
|
||||||
|
|
||||||
// Force model recompilation to ensure schema updates are applied
|
|
||||||
if (mongoose.models.PaintingScheme) {
|
|
||||||
delete mongoose.models.PaintingScheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default mongoose.model<IPaintingScheme>('PaintingScheme', PaintingSchemeSchema);
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export interface IPart extends Document {
|
|
||||||
projectId?: mongoose.Types.ObjectId;
|
|
||||||
description: string;
|
|
||||||
dimensions?: string | null;
|
|
||||||
weight?: number | null;
|
|
||||||
type?: string | null;
|
|
||||||
area?: number | null;
|
|
||||||
complexity?: number | null;
|
|
||||||
quantity: number;
|
|
||||||
notes?: string | null;
|
|
||||||
organizationId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PartSchema: Schema = new Schema({
|
|
||||||
organizationId: { type: String, index: true },
|
|
||||||
projectId: { type: Schema.Types.ObjectId, ref: 'Project', required: false },
|
|
||||||
description: { type: String, required: true },
|
|
||||||
dimensions: { type: String },
|
|
||||||
weight: { type: Number },
|
|
||||||
type: { type: String },
|
|
||||||
area: { type: Number },
|
|
||||||
complexity: { type: Number },
|
|
||||||
quantity: { type: Number, required: true, default: 1 },
|
|
||||||
notes: { type: String },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default mongoose.models.Part || mongoose.model<IPart>('Part', PartSchema);
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export interface IProject extends Document {
|
|
||||||
name: string;
|
|
||||||
client: string;
|
|
||||||
startDate?: Date | null;
|
|
||||||
endDate?: Date | null;
|
|
||||||
technician?: string | null;
|
|
||||||
environment?: string | null;
|
|
||||||
organizationId?: string;
|
|
||||||
weightKg?: number | null;
|
|
||||||
status: 'active' | 'archived';
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProjectSchema: Schema = new Schema({
|
|
||||||
name: { type: String, required: true },
|
|
||||||
client: { type: String, required: true },
|
|
||||||
organizationId: { type: String, index: true },
|
|
||||||
startDate: { type: Date },
|
|
||||||
endDate: { type: Date },
|
|
||||||
technician: { type: String },
|
|
||||||
environment: { type: String },
|
|
||||||
weightKg: { type: Number },
|
|
||||||
status: { type: String, enum: ['active', 'archived'], default: 'active', index: true },
|
|
||||||
}, { timestamps: true });
|
|
||||||
|
|
||||||
export default mongoose.models.Project || mongoose.model<IProject>('Project', ProjectSchema);
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export interface IStockAuditLog extends Document {
|
|
||||||
organizationId?: string;
|
|
||||||
stockItemId: mongoose.Types.ObjectId;
|
|
||||||
movementId?: mongoose.Types.ObjectId; // Optional, might be deleted
|
|
||||||
movementNumber?: number;
|
|
||||||
userId: string;
|
|
||||||
userName: string;
|
|
||||||
action: 'CREATE' | 'UPDATE' | 'DELETE';
|
|
||||||
details: string; // Human readable summary
|
|
||||||
oldValues?: Record<string, any>;
|
|
||||||
newValues?: Record<string, any>;
|
|
||||||
timestamp: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StockAuditLogSchema: Schema = new Schema({
|
|
||||||
organizationId: { type: String, index: true },
|
|
||||||
stockItemId: { type: Schema.Types.ObjectId, ref: 'StockItem', required: true },
|
|
||||||
movementId: { type: Schema.Types.ObjectId, ref: 'StockMovement' },
|
|
||||||
movementNumber: { type: Number },
|
|
||||||
userId: { type: String, required: true },
|
|
||||||
userName: { type: String, required: true },
|
|
||||||
action: { type: String, required: true, enum: ['CREATE', 'UPDATE', 'DELETE'] },
|
|
||||||
details: { type: String, required: true },
|
|
||||||
oldValues: { type: Object },
|
|
||||||
newValues: { type: Object },
|
|
||||||
timestamp: { type: Date, default: Date.now }
|
|
||||||
}, { timestamps: true });
|
|
||||||
|
|
||||||
export default mongoose.models.StockAuditLog || mongoose.model<IStockAuditLog>('StockAuditLog', StockAuditLogSchema);
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export interface IStockItem extends Document {
|
|
||||||
organizationId?: string;
|
|
||||||
createdBy?: string;
|
|
||||||
dataSheetId: mongoose.Types.ObjectId;
|
|
||||||
rrNumber: string; // Registro de Rastreabilidade
|
|
||||||
batchNumber: string; // Lote
|
|
||||||
color?: string;
|
|
||||||
invoiceNumber?: string; // Nota Fiscal
|
|
||||||
receivedBy?: string; // Quem recebeu
|
|
||||||
quantity: number;
|
|
||||||
unit: string;
|
|
||||||
minStock?: number; // Estoque mínimo estipulado
|
|
||||||
expirationDate?: Date;
|
|
||||||
entryDate: Date;
|
|
||||||
notes?: string;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StockItemSchema: Schema = new Schema({
|
|
||||||
organizationId: { type: String, index: true },
|
|
||||||
createdBy: { type: String, index: true },
|
|
||||||
dataSheetId: { type: Schema.Types.ObjectId, ref: 'TechnicalDataSheet', required: true },
|
|
||||||
rrNumber: { type: String, required: true },
|
|
||||||
batchNumber: { type: String, required: true },
|
|
||||||
color: { type: String },
|
|
||||||
invoiceNumber: { type: String },
|
|
||||||
receivedBy: { type: String },
|
|
||||||
quantity: { type: Number, required: true, default: 0 },
|
|
||||||
unit: { type: String, required: true },
|
|
||||||
minStock: { type: Number, default: 0 },
|
|
||||||
expirationDate: { type: Date },
|
|
||||||
entryDate: { type: Date, default: Date.now },
|
|
||||||
notes: { type: String }
|
|
||||||
}, { timestamps: true });
|
|
||||||
|
|
||||||
// Compound index to prevent duplicate RR within an organization, if desirable.
|
|
||||||
// For now, indexing RR for fast lookup.
|
|
||||||
StockItemSchema.index({ organizationId: 1, rrNumber: 1 });
|
|
||||||
|
|
||||||
export default mongoose.models.StockItem || mongoose.model<IStockItem>('StockItem', StockItemSchema);
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export type MovementType = 'ENTRY' | 'ADJUSTMENT' | 'CONSUMPTION';
|
|
||||||
|
|
||||||
export interface IStockMovement extends Document {
|
|
||||||
organizationId?: string;
|
|
||||||
createdBy?: string;
|
|
||||||
stockItemId: mongoose.Types.ObjectId;
|
|
||||||
movementNumber?: number;
|
|
||||||
type: MovementType;
|
|
||||||
quantity: number; // Positive for entry, negative for exit
|
|
||||||
date: Date;
|
|
||||||
responsible: string; // User who performed the action
|
|
||||||
reason?: string; // For ADJUSTMENT
|
|
||||||
requester?: string; // For CONSUMPTION
|
|
||||||
notes?: string;
|
|
||||||
createdAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StockMovementSchema: Schema = new Schema({
|
|
||||||
organizationId: { type: String, index: true },
|
|
||||||
createdBy: { type: String, index: true },
|
|
||||||
stockItemId: { type: Schema.Types.ObjectId, ref: 'StockItem', required: true },
|
|
||||||
movementNumber: { type: Number },
|
|
||||||
type: { type: String, enum: ['ENTRY', 'ADJUSTMENT', 'CONSUMPTION'], required: true },
|
|
||||||
quantity: { type: Number, required: true },
|
|
||||||
date: { type: Date, default: Date.now },
|
|
||||||
responsible: { type: String, required: true },
|
|
||||||
reason: { type: String },
|
|
||||||
requester: { type: String },
|
|
||||||
notes: { type: String }
|
|
||||||
}, { timestamps: true });
|
|
||||||
|
|
||||||
export default mongoose.models.StockMovement || mongoose.model<IStockMovement>('StockMovement', StockMovementSchema);
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export interface IStoredFile extends Document {
|
|
||||||
filename: string;
|
|
||||||
contentType: string;
|
|
||||||
data: Buffer;
|
|
||||||
size: number;
|
|
||||||
uploadDate: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StoredFileSchema: Schema = new Schema({
|
|
||||||
filename: { type: String, required: true },
|
|
||||||
contentType: { type: String, required: true },
|
|
||||||
data: { type: Buffer, required: true },
|
|
||||||
size: { type: Number, required: true },
|
|
||||||
uploadDate: { type: Date, default: Date.now }
|
|
||||||
});
|
|
||||||
|
|
||||||
export default mongoose.models.StoredFile || mongoose.model<IStoredFile>('StoredFile', StoredFileSchema);
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export interface ISystemSettings extends Document {
|
|
||||||
settingsId: string;
|
|
||||||
appName: string;
|
|
||||||
appSubtitle: string;
|
|
||||||
appLogoUrl?: string;
|
|
||||||
updatedBy?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SystemSettingsSchema: Schema = new Schema({
|
|
||||||
settingsId: { type: String, required: true, unique: true, default: 'global' },
|
|
||||||
appName: { type: String, required: true, default: 'GPI' },
|
|
||||||
appSubtitle: { type: String, required: true, default: 'Gestão de Pintura Industrial' },
|
|
||||||
appLogoUrl: { type: String },
|
|
||||||
updatedBy: { type: String } // Email of the dev who updated it
|
|
||||||
}, { timestamps: true });
|
|
||||||
|
|
||||||
export default mongoose.models.SystemSettings || mongoose.model<ISystemSettings>('SystemSettings', SystemSettingsSchema);
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export interface ITechnicalDataSheet extends Document {
|
|
||||||
name: string;
|
|
||||||
manufacturer?: string;
|
|
||||||
type?: string;
|
|
||||||
fileId?: mongoose.Types.ObjectId;
|
|
||||||
fileUrl: string;
|
|
||||||
uploadDate: Date;
|
|
||||||
solidsVolume?: number;
|
|
||||||
density?: number;
|
|
||||||
mixingRatio?: string;
|
|
||||||
mixingRatioWeight?: string;
|
|
||||||
mixingRatioVolume?: string;
|
|
||||||
wftMin?: number;
|
|
||||||
wftMax?: number;
|
|
||||||
dftMin?: number;
|
|
||||||
dftMax?: number;
|
|
||||||
reducer?: string;
|
|
||||||
yieldTheoretical?: number;
|
|
||||||
dftReference?: number;
|
|
||||||
yieldFactor?: number;
|
|
||||||
dilution?: number;
|
|
||||||
notes?: string;
|
|
||||||
organizationId?: string;
|
|
||||||
manufacturerCode?: string;
|
|
||||||
minStock?: number;
|
|
||||||
typicalApplication?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TechnicalDataSheetSchema: Schema = new Schema({
|
|
||||||
organizationId: { type: String, index: true },
|
|
||||||
name: { type: String, required: true },
|
|
||||||
manufacturer: { type: String },
|
|
||||||
manufacturerCode: { type: String },
|
|
||||||
type: { type: String },
|
|
||||||
minStock: { type: Number },
|
|
||||||
typicalApplication: { type: String },
|
|
||||||
fileId: { type: Schema.Types.ObjectId, ref: 'StoredFile' },
|
|
||||||
fileUrl: { type: String },
|
|
||||||
uploadDate: { type: Date, default: Date.now },
|
|
||||||
solidsVolume: { type: Number },
|
|
||||||
density: { type: Number },
|
|
||||||
mixingRatio: { type: String },
|
|
||||||
mixingRatioWeight: { type: String },
|
|
||||||
mixingRatioVolume: { type: String },
|
|
||||||
wftMin: { type: Number },
|
|
||||||
wftMax: { type: Number },
|
|
||||||
dftMin: { type: Number },
|
|
||||||
dftMax: { type: Number },
|
|
||||||
reducer: { type: String },
|
|
||||||
yieldTheoretical: { type: Number },
|
|
||||||
dftReference: { type: Number },
|
|
||||||
yieldFactor: { type: Number },
|
|
||||||
dilution: { type: Number },
|
|
||||||
notes: { type: String },
|
|
||||||
}, { timestamps: true });
|
|
||||||
|
|
||||||
export default mongoose.models.TechnicalDataSheet || mongoose.model<ITechnicalDataSheet>('TechnicalDataSheet', TechnicalDataSheetSchema);
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export type UserRole = 'guest' | 'user' | 'admin';
|
|
||||||
|
|
||||||
export interface IUser extends Document {
|
|
||||||
clerkId?: string;
|
|
||||||
logtoId?: string;
|
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
role: UserRole;
|
|
||||||
isBanned: boolean;
|
|
||||||
organizationId?: string;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
lastSeenAt?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserSchema: Schema = new Schema({
|
|
||||||
clerkId: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
unique: true,
|
|
||||||
sparse: true,
|
|
||||||
index: true
|
|
||||||
},
|
|
||||||
logtoId: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
unique: true,
|
|
||||||
sparse: true,
|
|
||||||
index: true
|
|
||||||
},
|
|
||||||
organizationId: {
|
|
||||||
type: String,
|
|
||||||
index: true
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
unique: true
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
role: {
|
|
||||||
type: String,
|
|
||||||
enum: ['guest', 'user', 'admin'],
|
|
||||||
default: 'guest'
|
|
||||||
},
|
|
||||||
isBanned: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
lastSeenAt: {
|
|
||||||
type: Date,
|
|
||||||
default: Date.now
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
timestamps: true
|
|
||||||
});
|
|
||||||
|
|
||||||
export default mongoose.models.User || mongoose.model<IUser>('User', UserSchema);
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export interface IPieceCategory {
|
|
||||||
id: string; // Keep as string for internal mapping if needed, or convert to Sub-document
|
|
||||||
name: string;
|
|
||||||
organizationId?: string;
|
|
||||||
weight: number;
|
|
||||||
area?: number; // Área em m² para cálculo alternativo
|
|
||||||
historicalYield: number;
|
|
||||||
historicalDft: number;
|
|
||||||
efficiency: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PieceCategorySchema: Schema = new Schema({
|
|
||||||
name: { type: String, required: true },
|
|
||||||
weight: { type: Number, required: true },
|
|
||||||
area: { type: Number }, // Área em m² (opcional)
|
|
||||||
historicalYield: { type: Number, required: true },
|
|
||||||
historicalDft: { type: Number, required: true },
|
|
||||||
efficiency: { type: Number, required: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface IYieldStudy extends Document {
|
|
||||||
name: string;
|
|
||||||
organizationId?: string;
|
|
||||||
dataSheetId: mongoose.Types.ObjectId;
|
|
||||||
targetDft: number;
|
|
||||||
dilutionPercent: number;
|
|
||||||
categories: IPieceCategory[];
|
|
||||||
totalWeight: number;
|
|
||||||
estimatedPaintVolume: number;
|
|
||||||
estimatedReducerVolume: number;
|
|
||||||
estimatedPaintVolumeByArea?: number; // Cálculo por área (m²)
|
|
||||||
estimatedReducerVolumeByArea?: number; // Cálculo por área (m²)
|
|
||||||
averageComplexity: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const YieldStudySchema: Schema = new Schema({
|
|
||||||
name: { type: String, required: true },
|
|
||||||
organizationId: { type: String, index: true },
|
|
||||||
dataSheetId: { type: Schema.Types.ObjectId, ref: 'TechnicalDataSheet', required: true },
|
|
||||||
targetDft: { type: Number, required: true },
|
|
||||||
dilutionPercent: { type: Number, default: 0 },
|
|
||||||
categories: [PieceCategorySchema],
|
|
||||||
totalWeight: { type: Number },
|
|
||||||
estimatedPaintVolume: { type: Number },
|
|
||||||
estimatedReducerVolume: { type: Number },
|
|
||||||
estimatedPaintVolumeByArea: { type: Number }, // Cálculo por área
|
|
||||||
estimatedReducerVolumeByArea: { type: Number }, // Cálculo por área
|
|
||||||
averageComplexity: { type: Number },
|
|
||||||
}, { timestamps: true });
|
|
||||||
|
|
||||||
export default mongoose.models.YieldStudy || mongoose.model<IYieldStudy>('YieldStudy', YieldStudySchema);
|
|
||||||
Reference in New Issue
Block a user