Minimax correcao

This commit is contained in:
2026-04-02 11:45:46 +00:00
parent ca2bdc19ab
commit 3132bb73a2
30 changed files with 118 additions and 1337 deletions

245
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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 />} />

View File

@@ -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>

View File

@@ -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)) {

View File

@@ -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;
}
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 }) => {
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,
};
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(() => {
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}>

View File

@@ -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 />)

View File

@@ -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();

View File

@@ -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';

View File

@@ -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);

View File

@@ -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();
};

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);