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",
|
||||
"jose": "^5.2.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
"mongodb": "^7.1.1",
|
||||
"multer": "^2.0.2",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
@@ -56,7 +55,6 @@
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"mongoose": "^8.23.0",
|
||||
"nodemon": "^3.1.11",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
@@ -2391,13 +2389,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@mongodb-js/saslprep": {
|
||||
"version": "1.4.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sparse-bitfield": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"dev": true,
|
||||
@@ -3604,17 +3595,6 @@
|
||||
"version": "10.0.0",
|
||||
"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": {
|
||||
"version": "8.18.1",
|
||||
"license": "MIT",
|
||||
@@ -5042,13 +5022,6 @@
|
||||
"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": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT"
|
||||
@@ -8037,10 +8010,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/memory-pager": {
|
||||
"version": "1.5.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
@@ -8164,179 +8133,6 @@
|
||||
"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": {
|
||||
"version": "1.2.0",
|
||||
"dev": true,
|
||||
@@ -8973,6 +8769,7 @@
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -9791,11 +9588,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/sift": {
|
||||
"version": "17.1.3",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"license": "ISC",
|
||||
@@ -9903,13 +9695,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sparse-bitfield": {
|
||||
"version": "3.0.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"memory-pager": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"license": "MIT",
|
||||
@@ -10299,16 +10084,6 @@
|
||||
"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": {
|
||||
"version": "1.2.2",
|
||||
"dev": true,
|
||||
@@ -11351,24 +11126,6 @@
|
||||
"version": "1.8.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": {
|
||||
"version": "2.0.2",
|
||||
"license": "ISC",
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
"express": "^5.2.1",
|
||||
"jose": "^5.2.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
"mongodb": "^7.1.1",
|
||||
"multer": "^2.0.2",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
@@ -65,7 +64,6 @@
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"mongoose": "^8.23.0",
|
||||
"nodemon": "^3.1.11",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
|
||||
@@ -17,8 +17,6 @@ import { DeveloperDashboard } from './pages/DeveloperDashboard';
|
||||
import { CalculatorDashboard } from './pages/CalculatorDashboard';
|
||||
import { StockDashboard } from './pages/StockDashboard';
|
||||
import { GuestDashboard } from './pages/GuestDashboard';
|
||||
import { Login } from './pages/Login';
|
||||
import { Callback } from './pages/Callback';
|
||||
import InstrumentList from './pages/InstrumentList';
|
||||
|
||||
const DeveloperRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
@@ -39,8 +37,6 @@ const AppContent: React.FC = () => {
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/" element={<ProjectList />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/callback" element={<Callback />} />
|
||||
<Route path="/guest-dashboard" element={<GuestDashboard />} />
|
||||
<Route path="/projects" element={<ProjectList />} />
|
||||
<Route path="/project/:id" element={<ProjectDetails />} />
|
||||
|
||||
@@ -2,9 +2,8 @@ import React, { useState } from 'react';
|
||||
import NotificationBell from './NotificationBell';
|
||||
import { TeamPresence } from './TeamPresence';
|
||||
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 { useLogto } from '@logto/react';
|
||||
import { TechnicalManual } from './TechnicalManual';
|
||||
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);
|
||||
});
|
||||
const location = useLocation();
|
||||
const { signOut } = useLogto();
|
||||
const { isAdmin, isUser, isDeveloper, appUser, isSignedIn } = useAuth();
|
||||
const { isAdmin, isUser, isDeveloper, appUser } = useAuth();
|
||||
|
||||
// Helper to get role display name
|
||||
const getRoleDisplay = () => {
|
||||
@@ -54,14 +52,6 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
}
|
||||
}, [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 {
|
||||
icon: React.ElementType;
|
||||
label: string;
|
||||
@@ -87,10 +77,6 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
signOut(window.location.origin);
|
||||
};
|
||||
|
||||
if (location.pathname === '/login' || location.pathname === '/callback') {
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -216,14 +202,6 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
)}
|
||||
</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="flex items-center gap-2">
|
||||
<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 ? 'Modo Claro' : 'Modo Escuro'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center gap-4 w-full text-text-main font-bold"
|
||||
>
|
||||
<LogOut size={20} />
|
||||
Sair
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -23,21 +23,7 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||
requireEdit = false,
|
||||
redirectTo = '/',
|
||||
}) => {
|
||||
const { appUser, isLoading, canEdit, isSignedIn } = 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 />;
|
||||
}
|
||||
const { appUser, canEdit } = useAuth();
|
||||
|
||||
// Check role-based access
|
||||
if (allowedRoles && appUser && !allowedRoles.includes(appUser.role)) {
|
||||
|
||||
@@ -1,120 +1,53 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useLogto } from '@logto/react';
|
||||
import type { AppUser } from '../types';
|
||||
import type { AppUser, UserRole } from '../types';
|
||||
import { AuthContext } from './AuthContextType';
|
||||
import { setUser } from '../main';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || '/api';
|
||||
import { getUser } from '../main';
|
||||
|
||||
interface AuthProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
const { isAuthenticated, getAccessToken, fetchUserInfo, isLoading: isLogtoLoading } = useLogto();
|
||||
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,
|
||||
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()
|
||||
};
|
||||
|
||||
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]);
|
||||
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
const [appUser, setAppUser] = useState<AppUser | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLogtoLoading) {
|
||||
syncUser();
|
||||
const storedUser = getUser();
|
||||
if (storedUser) {
|
||||
setAppUser({ ...defaultUser, ...storedUser, role: storedUser.role as UserRole });
|
||||
} else {
|
||||
setAppUser(defaultUser);
|
||||
}
|
||||
}, [isLogtoLoading, syncUser]);
|
||||
}, []);
|
||||
|
||||
const refetchUser = useCallback(async () => {
|
||||
await syncUser();
|
||||
}, [syncUser]);
|
||||
|
||||
const isDeveloper = useCallback(() => {
|
||||
return appUser?.email === 'admtracksteel@gmail.com';
|
||||
}, [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 isDeveloper = useCallback(() => false, []);
|
||||
const isAdmin = useCallback(() => true, []);
|
||||
const isUser = useCallback(() => true, []);
|
||||
const isGuest = useCallback(() => false, []);
|
||||
const canEdit = useCallback(() => true, []);
|
||||
const refetchUser = useCallback(async () => {}, []);
|
||||
|
||||
const value = useMemo(() => ({
|
||||
appUser,
|
||||
isLoading,
|
||||
isSignedIn: isAuthenticated,
|
||||
error,
|
||||
isLoading: false,
|
||||
isSignedIn: true,
|
||||
error: null,
|
||||
isAdmin,
|
||||
isUser,
|
||||
isGuest,
|
||||
isDeveloper,
|
||||
canEdit,
|
||||
refetchUser,
|
||||
}), [appUser, isLoading, isAuthenticated, error, isAdmin, isUser, isGuest, isDeveloper, canEdit, refetchUser]);
|
||||
}), [appUser, isAdmin, isUser, isGuest, isDeveloper, canEdit, refetchUser]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
|
||||
@@ -1,34 +1,22 @@
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { LogtoProvider, type LogtoConfig } from '@logto/react';
|
||||
import './index.css'
|
||||
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() {
|
||||
return sessionStorage.getItem('logto_token');
|
||||
return 'guest-token';
|
||||
}
|
||||
|
||||
export function getUser() {
|
||||
const user = sessionStorage.getItem('logto_user');
|
||||
return user ? JSON.parse(user) : null;
|
||||
return {
|
||||
id: 'guest-user',
|
||||
email: 'guest@gpi.app',
|
||||
name: 'Guest User',
|
||||
role: 'user'
|
||||
};
|
||||
}
|
||||
|
||||
export function setUser(token: string, user: any) {
|
||||
sessionStorage.setItem('logto_token', token);
|
||||
sessionStorage.setItem('logto_user', JSON.stringify(user));
|
||||
console.log('User set (no auth):', user);
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<LogtoProvider config={config}>
|
||||
<App />
|
||||
</LogtoProvider>
|
||||
)
|
||||
createRoot(document.getElementById('root')!).render(<App />)
|
||||
@@ -5,7 +5,7 @@ import '../middleware/authMiddleware.js'; // Ensure type augmentation
|
||||
export const createApplicationRecord = async (req: Request, res: Response) => {
|
||||
try {
|
||||
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 });
|
||||
res.status(201).json(record);
|
||||
} catch (error: unknown) {
|
||||
@@ -29,17 +29,10 @@ export const getApplicationRecordsByProject = async (req: Request, res: Response
|
||||
export const updateApplicationRecord = async (req: Request, res: Response) => {
|
||||
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 record = await appRecordService.updateApplicationRecord(
|
||||
req.params.id as string,
|
||||
req.body,
|
||||
organizationId,
|
||||
userId,
|
||||
userRole as any,
|
||||
isDeveloper
|
||||
req.body
|
||||
);
|
||||
if (!record) return res.status(403).json({ error: 'Não autorizado ou não encontrado.' });
|
||||
res.json(record);
|
||||
@@ -51,17 +44,8 @@ export const updateApplicationRecord = async (req: Request, res: Response) => {
|
||||
|
||||
export const deleteApplicationRecord = async (req: Request, res: Response) => {
|
||||
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(
|
||||
req.params.id as string,
|
||||
organizationId,
|
||||
userId,
|
||||
userRole as any,
|
||||
isDeveloper
|
||||
req.params.id as string
|
||||
);
|
||||
if (!success) return res.status(403).json({ error: 'Não autorizado ou não encontrado.' });
|
||||
res.status(204).send();
|
||||
|
||||
@@ -6,7 +6,7 @@ import '../middleware/authMiddleware.js'; // Ensure type augmentation
|
||||
export const createInspection = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const organizationId = req.appUser?.organizationId;
|
||||
const createdBy = req.appUser?.clerkId;
|
||||
const createdBy = req.appUser?.email || 'guest';
|
||||
const inspection = await inspectionService.createInspection({
|
||||
...req.body,
|
||||
organizationId,
|
||||
@@ -46,17 +46,10 @@ export const getInspectionsByProject = async (req: Request, res: Response) => {
|
||||
export const updateInspection = async (req: Request, res: Response) => {
|
||||
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 inspection = await inspectionService.updateInspection(
|
||||
req.params.id as string,
|
||||
req.body,
|
||||
organizationId,
|
||||
userId,
|
||||
userRole as any,
|
||||
isDeveloper
|
||||
req.body
|
||||
);
|
||||
if (!inspection) return res.status(403).json({ error: 'Não autorizado ou não encontrado.' });
|
||||
res.json(inspection);
|
||||
@@ -69,16 +62,9 @@ export const updateInspection = async (req: Request, res: Response) => {
|
||||
export const deleteInspection = async (req: Request, res: Response) => {
|
||||
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 inspectionService.deleteInspection(
|
||||
req.params.id as string,
|
||||
organizationId,
|
||||
userId,
|
||||
userRole as any,
|
||||
isDeveloper
|
||||
req.params.id as string
|
||||
);
|
||||
if (!success) return res.status(403).json({ error: 'Não autorizado ou não encontrado.' });
|
||||
res.status(204).send();
|
||||
@@ -91,7 +77,9 @@ export const deleteInspection = async (req: Request, res: Response) => {
|
||||
export const getAllInspections = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const organizationId = req.appUser?.organizationId;
|
||||
const inspections = await inspectionService.getAllInspections(organizationId);
|
||||
const inspections = organizationId
|
||||
? await inspectionService.getInspectionsByOrganization(organizationId)
|
||||
: await inspectionService.getInspectionStats();
|
||||
res.json(inspections);
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Request, Response } from 'express';
|
||||
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 fs from 'fs';
|
||||
import os from 'os';
|
||||
@@ -29,16 +30,28 @@ export const updateSettings = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { appName, appSubtitle, appLogoUrl } = req.body;
|
||||
|
||||
const settings = await SystemSettings.findOneAndUpdate(
|
||||
{ settingsId: 'global' },
|
||||
const existing = await SystemSettings.findOne({ settingsId: 'global' });
|
||||
|
||||
let settings;
|
||||
if (!existing) {
|
||||
settings = await SystemSettings.create({
|
||||
settingsId: 'global',
|
||||
appName,
|
||||
appSubtitle,
|
||||
appLogoUrl,
|
||||
updatedBy: req.appUser?.email
|
||||
});
|
||||
} else {
|
||||
settings = await SystemSettings.findByIdAndUpdate(
|
||||
existing.id,
|
||||
{
|
||||
appName,
|
||||
appSubtitle,
|
||||
appLogoUrl,
|
||||
updatedBy: req.appUser?.email
|
||||
},
|
||||
{ new: true, upsert: true } // Create if not exists
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`⚙️ System Settings updated by ${req.appUser?.email}`);
|
||||
res.json(settings);
|
||||
@@ -92,8 +105,13 @@ export const uploadLogo = async (req: Request, res: Response) => {
|
||||
// Global Admin Functions
|
||||
export const getGlobalUsers = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const users = await User.find({}).sort({ createdAt: -1 });
|
||||
res.json(users);
|
||||
const { data: users, error } = await supabase
|
||||
.from('users')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
res.json(users || []);
|
||||
} catch (error) {
|
||||
console.error('Error getting global users:', error);
|
||||
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) => {
|
||||
try {
|
||||
// Aggregate members to group by org and get full member lists
|
||||
const organizations = await OrganizationMember.aggregate([
|
||||
{
|
||||
$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 } }
|
||||
]);
|
||||
const { data: organizations, error } = await supabase
|
||||
.from('organizations')
|
||||
.select('*');
|
||||
|
||||
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) {
|
||||
console.error('Error getting global organizations:', error);
|
||||
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.' });
|
||||
}
|
||||
|
||||
// Upsert the Organization record
|
||||
const org = await Organization.findOneAndUpdate(
|
||||
{ clerkId: organizationId },
|
||||
{ isBanned: isBanned },
|
||||
{ new: true, upsert: true }
|
||||
);
|
||||
const { data: org, error } = await supabase
|
||||
.from('organizations')
|
||||
.update({ is_banned: isBanned })
|
||||
.eq('id', organizationId)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
console.log(`Organization ${organizationId} ban status set to ${isBanned} by ${req.appUser?.email}`);
|
||||
res.json(org);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { authenticateRequest } from './logtoAuth.js';
|
||||
import { findOneGpi } from '../config/supabase.js';
|
||||
|
||||
export interface IAppUser {
|
||||
id: string;
|
||||
@@ -19,24 +17,16 @@ declare module 'express-serve-static-core' {
|
||||
}
|
||||
|
||||
export const extractUser = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const user = await authenticateRequest(req);
|
||||
|
||||
if (user) {
|
||||
req.appUser = user;
|
||||
}
|
||||
|
||||
req.appUser = {
|
||||
id: 'guest-user',
|
||||
logtoId: 'guest',
|
||||
email: 'guest@gpi.app',
|
||||
name: 'Guest User',
|
||||
role: 'user',
|
||||
organizationId: 'default-org',
|
||||
organizationRole: 'user'
|
||||
};
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Error extracting user:', error);
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
export const requireRole = (allowedRoles: string[]) => {
|
||||
@@ -44,17 +34,6 @@ export const requireRole = (allowedRoles: string[]) => {
|
||||
if (!req.appUser) {
|
||||
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();
|
||||
};
|
||||
};
|
||||
@@ -63,26 +42,9 @@ export const requireAdmin = requireRole(['admin']);
|
||||
export const requireUser = requireRole(['user', 'admin']);
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
@@ -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