diff --git a/src/server/controllers/geometryTypeController.ts b/src/server/controllers/geometryTypeController.ts index 2fa337a..0c932cd 100644 --- a/src/server/controllers/geometryTypeController.ts +++ b/src/server/controllers/geometryTypeController.ts @@ -1,5 +1,6 @@ import { Request, Response } from 'express'; -import GeometryType from '../models/GeometryType.js'; +import { GeometryType } from '../lib/compat.js'; + import { IAppUser } from '../middleware/authMiddleware.js'; interface AuthRequest extends Request { @@ -33,11 +34,11 @@ export const getAllnames = async (req: AuthRequest, res: Response) => { } // Search for org-specific types OR orphan types (legacy) - const query = isGlobalAdmin + const filter = isGlobalAdmin ? {} - : { $or: [{ organizationId }, { organizationId: { $exists: false } }, { organizationId: null }] }; + : { organizationId }; - let types = await GeometryType.find(query).sort({ name: 1 }); + let types = await GeometryType.find(filter); // Auto-seed if empty AND we HAVE an organization (don't seed for global view) if (types.length === 0 && organizationId) { @@ -91,13 +92,11 @@ export const createType = async (req: AuthRequest, res: Response) => { return res.status(400).json({ error: 'Name is required' }); } - const newType = new GeometryType({ + const saved = await GeometryType.create({ name, efficiencyLoss: Number(efficiencyLoss) || 0, organizationId }); - - const saved = await newType.save(); res.status(201).json(saved); } catch (error: unknown) { const message = error instanceof Error ? error.message : 'Unknown error'; @@ -115,14 +114,9 @@ export const updateType = async (req: AuthRequest, res: Response) => { const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com'; const { name, efficiencyLoss } = req.body; - const query = isGlobalAdmin - ? { _id: id } - : { _id: id, organizationId }; - const updated = await GeometryType.findOneAndUpdate( - query, - { name, efficiencyLoss: Number(efficiencyLoss) }, - { new: true } + { id, organizationId }, + { name, efficiencyLoss: Number(efficiencyLoss) } ); if (!updated) { @@ -142,11 +136,7 @@ export const deleteType = async (req: AuthRequest, res: Response) => { const organizationId = req.appUser?.organizationId; const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com'; - const query = isGlobalAdmin - ? { _id: id } - : { _id: id, organizationId }; - - const deleted = await GeometryType.findOneAndDelete(query); + const deleted = await GeometryType.findOneAndDelete({ id, organizationId }); if (!deleted) { return res.status(404).json({ error: 'Record not found' }); diff --git a/src/server/controllers/instrumentController.ts b/src/server/controllers/instrumentController.ts index 7b16239..9227b3a 100644 --- a/src/server/controllers/instrumentController.ts +++ b/src/server/controllers/instrumentController.ts @@ -1,5 +1,6 @@ import { Request, Response } from 'express'; -import Instrument from '../models/Instrument.js'; +import { Instrument } from '../lib/compat.js'; + import { IAppUser } from '../middleware/authMiddleware.js'; interface AuthRequest extends Request { diff --git a/src/server/controllers/messageController.ts b/src/server/controllers/messageController.ts index cec2795..adf9bc9 100644 --- a/src/server/controllers/messageController.ts +++ b/src/server/controllers/messageController.ts @@ -1,12 +1,11 @@ import { Request, Response } from 'express'; -import Message from '../models/Message.js'; -import OrganizationMember from '../models/OrganizationMember.js'; +import { Message, OrganizationMember } from '../lib/compat.js'; // Send a message export const sendMessage = async (req: Request, res: Response) => { try { const { toUserId, message } = req.body; - const fromUserId = req.appUser?.clerkId; + const fromUserId = req.appUser?.id; const organizationId = req.headers['x-organization-id'] as string; if (!organizationId) { @@ -21,11 +20,7 @@ export const sendMessage = async (req: Request, res: Response) => { return res.status(400).json({ error: 'Destinatário e mensagem são obrigatórios.' }); } - if (message.length > 255) { - return res.status(400).json({ error: 'Mensagem muito longa (máximo 255 caracteres).' }); - } - - // Check if there's already a pending (unread) message from this user to that user + // Check if there's already a pending (unread) message const existingMessage = await Message.findOne({ organizationId, fromUserId, @@ -34,22 +29,21 @@ export const sendMessage = async (req: Request, res: Response) => { }); if (existingMessage) { - // Update existing message instead of creating a new one - existingMessage.message = message; - existingMessage.updatedAt = new Date(); - await existingMessage.save(); - return res.json(existingMessage); + const updated = await Message.findOneAndUpdate( + { id: existingMessage.id }, + { message, updatedAt: new Date() } + ); + return res.json(updated); } // Create new message - const newMessage = new Message({ + const newMessage = await Message.create({ organizationId, fromUserId, toUserId, message, }); - await newMessage.save(); res.status(201).json(newMessage); } catch (error) { console.error('Error sending message:', error); @@ -60,7 +54,7 @@ export const sendMessage = async (req: Request, res: Response) => { // Get unread messages for current user export const getUnreadMessages = async (req: Request, res: Response) => { try { - const toUserId = req.appUser?.clerkId; + const toUserId = req.appUser?.id; const organizationId = req.headers['x-organization-id'] as string; if (!organizationId) { @@ -77,14 +71,14 @@ export const getUnreadMessages = async (req: Request, res: Response) => { isRead: false, isArchived: false, isDeletedByRecipient: false, - }).sort({ createdAt: -1 }); + }); // Populate sender info const messagesWithSender = await Promise.all( - messages.map(async (msg) => { - const sender = await OrganizationMember.findOne({ clerkUserId: msg.fromUserId }); + messages.map(async (msg: any) => { + const sender = await OrganizationMember.findOne({ userId: msg.fromUserId }); return { - ...msg.toObject(), + ...msg, fromUser: sender ? { name: sender.name, email: sender.email } : null, }; }) @@ -101,32 +95,21 @@ export const getUnreadMessages = async (req: Request, res: Response) => { export const markMessageAsRead = async (req: Request, res: Response) => { try { const { id } = req.params; - const userId = req.appUser?.clerkId; + const userId = req.appUser?.id; const organizationId = req.headers['x-organization-id'] as string; - if (!organizationId) { - return res.status(400).json({ error: 'Organização não selecionada.' }); - } + if (!userId) return res.status(401).json({ error: 'Usuário não autenticado.' }); - if (!userId) { - return res.status(401).json({ error: 'Usuário não autenticado.' }); - } + const updated = await Message.findOneAndUpdate( + { id, organizationId, toUserId: userId }, + { isRead: true, readAt: new Date() } + ); - const message = await Message.findOne({ - _id: id, - organizationId, - toUserId: userId, - }); - - if (!message) { + if (!updated) { return res.status(404).json({ error: 'Mensagem não encontrada.' }); } - message.isRead = true; - message.readAt = new Date(); - await message.save(); - - res.json(message); + res.json(updated); } catch (error) { console.error('Error marking message as read:', error); res.status(500).json({ error: 'Erro ao marcar mensagem como lida.' }); @@ -136,29 +119,23 @@ export const markMessageAsRead = async (req: Request, res: Response) => { // Get my pending (unread) sent messages export const getMyPendingMessages = async (req: Request, res: Response) => { try { - const fromUserId = req.appUser?.clerkId; + const fromUserId = req.appUser?.id; const organizationId = req.headers['x-organization-id'] as string; - if (!organizationId) { - return res.status(400).json({ error: 'Organização não selecionada.' }); - } - - if (!fromUserId) { - return res.status(401).json({ error: 'Usuário não autenticado.' }); - } + if (!fromUserId) return res.status(401).json({ error: 'Usuário não autenticado.' }); const messages = await Message.find({ organizationId, fromUserId, isRead: false, - }).sort({ createdAt: -1 }); + }); // Populate recipient info const messagesWithRecipient = await Promise.all( - messages.map(async (msg) => { - const recipient = await OrganizationMember.findOne({ clerkUserId: msg.toUserId }); + messages.map(async (msg: any) => { + const recipient = await OrganizationMember.findOne({ userId: msg.toUserId }); return { - ...msg.toObject(), + ...msg, toUser: recipient ? { name: recipient.name, email: recipient.email } : null, }; }) @@ -171,54 +148,45 @@ export const getMyPendingMessages = async (req: Request, res: Response) => { } }; -// Delete a message (only if unread and sender is the current user) +// Delete a message export const deleteMessage = async (req: Request, res: Response) => { try { const { id } = req.params; - const userId = req.appUser?.clerkId; + const userId = req.appUser?.id; const organizationId = req.headers['x-organization-id'] as string; - if (!organizationId) { - return res.status(400).json({ error: 'Organização não selecionada.' }); - } - - if (!userId) { - return res.status(401).json({ error: 'Usuário não autenticado.' }); - } - - const message = await Message.findOne({ - _id: id, + const deleted = await Message.findOneAndDelete({ + id, organizationId, fromUserId: userId, - isRead: false, // Can only delete unread messages + isRead: false }); - if (!message) { + if (!deleted) { return res.status(404).json({ error: 'Mensagem não encontrada ou já foi lida.' }); } - await message.deleteOne(); - res.json({ message: 'Mensagem deletada com sucesso.' }); + res.status(204).send(); } catch (error) { console.error('Error deleting message:', error); res.status(500).json({ error: 'Erro ao deletar mensagem.' }); } }; -// Recipient deletes/archives a message +// Recipient archives a message export const archiveMessage = async (req: Request, res: Response) => { try { const { id } = req.params; - const userId = req.appUser?.clerkId; + const userId = req.appUser?.id; const organizationId = req.headers['x-organization-id'] as string; - const message = await Message.findOne({ _id: id, toUserId: userId, organizationId }); - if (!message) return res.status(404).json({ error: 'Mensagem não encontrada.' }); + const updated = await Message.findOneAndUpdate( + { id, toUserId: userId, organizationId }, + { isArchived: true, isRead: true } + ); - message.isArchived = true; - message.isRead = true; // Arquivar implica ler - await message.save(); - res.json(message); + if (!updated) return res.status(404).json({ error: 'Mensagem não encontrada.' }); + res.json(updated); } catch (error) { console.error('Error archiving message:', error); res.status(500).json({ error: 'Erro ao arquivar mensagem.' }); @@ -228,14 +196,15 @@ export const archiveMessage = async (req: Request, res: Response) => { export const recipientDeleteMessage = async (req: Request, res: Response) => { try { const { id } = req.params; - const userId = req.appUser?.clerkId; + const userId = req.appUser?.id; const organizationId = req.headers['x-organization-id'] as string; - const message = await Message.findOne({ _id: id, toUserId: userId, organizationId }); - if (!message) return res.status(404).json({ error: 'Mensagem não encontrada.' }); + const updated = await Message.findOneAndUpdate( + { id, toUserId: userId, organizationId }, + { isDeletedByRecipient: true } + ); - message.isDeletedByRecipient = true; - await message.save(); + if (!updated) return res.status(404).json({ error: 'Mensagem não encontrada.' }); res.json({ message: 'Mensagem excluída com sucesso.' }); } catch (error) { console.error('Error deleting message:', error); diff --git a/src/server/controllers/stockController.ts b/src/server/controllers/stockController.ts index 3820cb5..c6ac5a2 100644 --- a/src/server/controllers/stockController.ts +++ b/src/server/controllers/stockController.ts @@ -1,7 +1,5 @@ import { Request, Response } from 'express'; -import StockItem from '../models/StockItem.js'; -import StockMovement from '../models/StockMovement.js'; - +import { StockItem, StockMovement, StockAuditLog, TechnicalDataSheet } from '../lib/compat.js'; import { IAppUser } from '../middleware/authMiddleware.js'; import { notificationService } from '../services/notificationService.js'; @@ -27,47 +25,33 @@ export const createStockItem = async (req: AuthRequest, res: Response) => { minStock } = req.body; - // Validation if (!dataSheetId || !rrNumber || !batchNumber || quantity === undefined || !unit) { return res.status(400).json({ error: 'Campos obrigatórios: DataSheet, RR, Lote, Quantidade, Unidade.' }); } - // Check for duplicate RR within Org const existing = await StockItem.findOne({ organizationId, rrNumber }); if (existing) { return res.status(400).json({ error: `Já existe um item com o RR ${rrNumber}.` }); } - // --- Min Stock Inheritance Logic --- let finalMinStock = Number(minStock) || 0; - - // If user didn't provide a specific minStock (or provided 0), try to inherit from existing group if (finalMinStock === 0) { - const existingGroupItem = await StockItem.findOne({ - organizationId, - dataSheetId, - color - }).sort({ updatedAt: -1 }); // Get latest active config - - if (existingGroupItem && existingGroupItem.minStock > 0) { - finalMinStock = existingGroupItem.minStock; + const items = await StockItem.find({ organizationId, dataSheetId, color }); + if (items.length > 0) { + finalMinStock = items[0].minStock || 0; } } else { - // If user DID provide a minStock, update all existing items in that group to match? - // User requested: "a regra de estoque minimo definido no cadastro precisa estar clonado para novos cadastros" - // And "soma dessas 'mesmas' tintas sejam comparadas com o estoque minimo cadastrado a elas" - // This implies the rule is a Property of the Group. So create/update should enforce consistency. if (finalMinStock > 0) { await StockItem.updateMany( { organizationId, dataSheetId, color }, - { $set: { minStock: finalMinStock } } + { minStock: finalMinStock } ); } } - const newItem = new StockItem({ + const savedItem = await StockItem.create({ organizationId, - createdBy: req.appUser?.clerkId, + createdBy: req.appUser?.id, dataSheetId, rrNumber, batchNumber, @@ -81,13 +65,10 @@ export const createStockItem = async (req: AuthRequest, res: Response) => { receivedBy }); - const savedItem = await newItem.save(); - - // Create Initial Movement (ENTRY) await StockMovement.create({ organizationId, - createdBy: req.appUser?.clerkId, - stockItemId: savedItem._id, + createdBy: req.appUser?.id, + stockItemId: savedItem.id, movementNumber: 1, type: 'ENTRY', quantity: Number(quantity), @@ -95,25 +76,21 @@ export const createStockItem = async (req: AuthRequest, res: Response) => { notes: 'Abertura de Lote / Entrada Inicial' }); - // Notificação de Recebimento if (organizationId) { await notificationService.create({ organizationId, title: 'Recebimento de Material', message: `Recebido: ${quantity}${unit} de ${savedItem.rrNumber} (Lote: ${batchNumber}).`, type: 'info', - metadata: { stockItemId: savedItem._id, triggerType: 'stock_received' } + metadata: { stockItemId: savedItem.id, triggerType: 'stock_received' } }); + await notificationService.checkLowStock(savedItem.id); } - // Check Low Stock immediately - await notificationService.checkLowStock(savedItem._id.toString()); - res.status(201).json(savedItem); } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; console.error('Error creating stock item:', error); - res.status(500).json({ error: message }); + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }); } }; @@ -121,49 +98,29 @@ export const updateStockItem = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; const organizationId = req.appUser?.organizationId; - // Only allow updating metadata, NOT quantity directly (quantity must be via adjustments) - // Adjusting logic: Admin might need to fix typo in quantity without movement record? - // Better enforcing movements. If quantity changes, user should use "Adjustment". - // Here we create a general update for details like Notes, Dates, etc. - - const { quantity, ...otherData } = req.body; // Separate quantity + const { quantity, ...otherData } = req.body; if (quantity !== undefined) { return res.status(400).json({ error: 'Para alterar a quantidade, utilize as funções de Ajuste ou Consumo.' }); } - // Check if Min Stock is being updated if (otherData.minStock !== undefined) { - const item = await StockItem.findOne({ _id: id, organizationId }); + const item = await StockItem.findOne({ id, organizationId }); if (item) { - // Propagate to all siblings (same Product + Color) await StockItem.updateMany( - { - organizationId, - dataSheetId: item.dataSheetId, - color: item.color - }, - { $set: { minStock: otherData.minStock } } + { organizationId, dataSheetId: item.dataSheetId, color: item.color }, + { minStock: otherData.minStock } ); } } - const updated = await StockItem.findOneAndUpdate( - { _id: id, organizationId }, - otherData, - { new: true } - ); - + const updated = await StockItem.findOneAndUpdate({ id, organizationId }, otherData); if (!updated) return res.status(404).json({ error: 'Item não encontrado.' }); - // Check Low Stock (in case minStock changed) - await notificationService.checkLowStock(updated._id.toString()); - + await notificationService.checkLowStock(id); res.json(updated); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - res.status(500).json({ error: message }); + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }); } }; @@ -172,46 +129,36 @@ export const adjustStock = async (req: AuthRequest, res: Response) => { const { id } = req.params; const organizationId = req.appUser?.organizationId; const userName = req.appUser?.name || req.appUser?.email || 'Unknown User'; - const { quantityDelta, reason } = req.body; // quantityDelta: +10 or -5 + const { quantityDelta, reason } = req.body; if (!reason) return res.status(400).json({ error: 'Motivo é obrigatório para ajustes técnicos.' }); if (!quantityDelta || isNaN(quantityDelta)) return res.status(400).json({ error: 'Quantidade inválida.' }); - const item = await StockItem.findOne({ _id: id, organizationId }); + const item = await StockItem.findOne({ id, organizationId }); if (!item) return res.status(404).json({ error: 'Item não encontrado.' }); - // Calculate new quantity const newQuantity = Number(item.quantity) + Number(quantityDelta); if (newQuantity < 0) return res.status(400).json({ error: 'Estoque insuficiente para este ajuste.' }); - item.quantity = newQuantity; - await item.save(); + await StockItem.findOneAndUpdate({ id }, { quantity: newQuantity }); - // Calculate next movement number - const lastMov = await StockMovement.findOne({ stockItemId: item._id }).sort({ movementNumber: -1 }); - const count = await StockMovement.countDocuments({ stockItemId: item._id }); - const movementNumber = (lastMov?.movementNumber || count) + 1; - - // Register Movement + const count = await StockMovement.countDocuments({ stockItemId: id }); + await StockMovement.create({ organizationId, - createdBy: req.appUser?.clerkId, - stockItemId: item._id, - movementNumber, + createdBy: req.appUser?.id, + stockItemId: id, + movementNumber: count + 1, type: 'ADJUSTMENT', quantity: Number(quantityDelta), responsible: userName, reason }); - // Check Low Stock - await notificationService.checkLowStock(item._id.toString()); - - res.json(item); - + await notificationService.checkLowStock(id); + res.json({ ...item, quantity: newQuantity }); } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - res.status(500).json({ error: message }); + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }); } }; @@ -225,40 +172,32 @@ export const consumeStock = async (req: AuthRequest, res: Response) => { if (!requester) return res.status(400).json({ error: 'Solicitante é obrigatório.' }); if (!quantityConsumed || Number(quantityConsumed) <= 0) return res.status(400).json({ error: 'Quantidade deve ser maior que zero.' }); - const item = await StockItem.findOne({ _id: id, organizationId }); + const item = await StockItem.findOne({ id, organizationId }); if (!item) return res.status(404).json({ error: 'Item não encontrado.' }); if (item.quantity < Number(quantityConsumed)) return res.status(400).json({ error: 'Estoque insuficiente.' }); - item.quantity -= Number(quantityConsumed); - await item.save(); + const newQuantity = Number(item.quantity) - Number(quantityConsumed); + await StockItem.findOneAndUpdate({ id }, { quantity: newQuantity }); - // Calculate next movement number - const lastMov = await StockMovement.findOne({ stockItemId: item._id }).sort({ movementNumber: -1 }); - const count = await StockMovement.countDocuments({ stockItemId: item._id }); - const movementNumber = (lastMov?.movementNumber || count) + 1; + const count = await StockMovement.countDocuments({ stockItemId: id }); - // Register Movement (Negative quantity for consumption) await StockMovement.create({ organizationId, - createdBy: req.appUser?.clerkId, - stockItemId: item._id, - movementNumber, + createdBy: req.appUser?.id, + stockItemId: id, + movementNumber: count + 1, type: 'CONSUMPTION', - quantity: -Number(quantityConsumed), // Negative + quantity: -Number(quantityConsumed), responsible: userName, requester, date: date || new Date() }); - // Check Low Stock - await notificationService.checkLowStock(item._id.toString()); - - res.json(item); - + await notificationService.checkLowStock(id); + res.json({ ...item, quantity: newQuantity }); } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - res.status(500).json({ error: message }); + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }); } }; @@ -267,20 +206,15 @@ export const deleteStockItem = async (req: AuthRequest, res: Response) => { const { id } = req.params; const organizationId = req.appUser?.organizationId; - // Optional: Block delete if there are movements other than ENTRY? - // For simplicity allow Admin to nuke it. - - const deleted = await StockItem.findOneAndDelete({ _id: id, organizationId }); + const deleted = await StockItem.findOneAndDelete({ id, organizationId }); if (!deleted) return res.status(404).json({ error: 'Item não encontrado.' }); - // Cleanup movements & logs await StockMovement.deleteMany({ stockItemId: id }); await StockAuditLog.deleteMany({ stockItemId: id }); res.status(204).send(); } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - res.status(500).json({ error: message }); + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }); } }; @@ -289,19 +223,23 @@ export const getStockItems = async (req: AuthRequest, res: Response) => { const organizationId = req.appUser?.organizationId; const { dataSheetId } = req.query; - // eslint-disable-next-line @typescript-eslint/no-explicit-any const query: any = { organizationId }; if (dataSheetId) query.dataSheetId = dataSheetId; - // Sort by Expiration Date ASC (First to expire first) - const items = await StockItem.find(query) - .populate('dataSheetId', 'name manufacturer type') - .sort({ expirationDate: 1 }); + const items = await StockItem.find(query); - res.json(items); + // Manual population + const itemsWithDetails = await Promise.all(items.map(async (item: any) => { + if (item.dataSheetId) { + const ds = await TechnicalDataSheet.findById(item.dataSheetId); + return { ...item, dataSheetId: ds || item.dataSheetId }; + } + return item; + })); + + res.json(itemsWithDetails); } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - res.status(500).json({ error: message }); + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }); } }; @@ -310,119 +248,73 @@ export const getStockItemById = async (req: AuthRequest, res: Response) => { const { id } = req.params; const organizationId = req.appUser?.organizationId; - const item = await StockItem.findOne({ _id: id, organizationId }) - .populate('dataSheetId', 'name manufacturer type'); - + const item = await StockItem.findOne({ id, organizationId }); if (!item) return res.status(404).json({ error: 'Item não encontrado.' }); + if (item.dataSheetId) { + const ds = await TechnicalDataSheet.findById(item.dataSheetId); + item.dataSheetId = ds || item.dataSheetId; + } + res.json(item); } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - res.status(500).json({ error: message }); + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }); } }; export const getStockMovements = async (req: AuthRequest, res: Response) => { try { - const { id } = req.params; // StockItem ID + const { id } = req.params; const organizationId = req.appUser?.organizationId; - - const movements = await StockMovement.find({ stockItemId: id, organizationId }) - .sort({ date: -1 }); - + const movements = await StockMovement.find({ stockItemId: id, organizationId }); res.json(movements); } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - res.status(500).json({ error: message }); + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }); } }; -// ------------------------------------------------------------------ -// CRUD & Auditing for Movements -// ------------------------------------------------------------------ - -import StockAuditLog from '../models/StockAuditLog.js'; - export const updateStockMovement = async (req: AuthRequest, res: Response) => { try { - const { id } = req.params; // Movement ID + const { id } = req.params; const organizationId = req.appUser?.organizationId; const userName = req.appUser?.name || req.appUser?.email || 'Unknown User'; - const userId = req.appUser?.clerkId || 'system'; + const userId = req.appUser?.id || 'system'; const isAdmin = req.appUser?.role === 'admin' || req.appUser?.organizationRole === 'admin'; - if (!isAdmin) { - return res.status(403).json({ error: 'Apenas administradores podem editar movimentações.' }); - } + if (!isAdmin) return res.status(403).json({ error: 'No admin permissions.' }); const { date, quantity, notes } = req.body; + const movement = await StockMovement.findOne({ id, organizationId }); + if (!movement) return res.status(404).json({ error: 'Movement not found.' }); - const movement = await StockMovement.findOne({ _id: id, organizationId }); - if (!movement) return res.status(404).json({ error: 'Movimentação não encontrada.' }); - - const item = await StockItem.findOne({ _id: movement.stockItemId, organizationId }); - if (!item) return res.status(404).json({ error: 'Item de estoque associado não encontrado.' }); - - // Calculate Delta - // If quantity changed, we need to adjust the item balance - // Note: 'quantity' in movement is signed (+ for entry, - for consumption) - // If the user edits a Consumption (-10) to (-15), the val passed in body might be absolute or signed? - // Let's assume the frontend sends the SIGNED value consistent with the movement type? - // Actually best to stick to specific logic: - // If movement type is ENTRY/ADJUSTMENT, quantity is usually positive (unless neg adjustment). - // If CONSUMPTION, quantity is stored negative. - // Let's expect the frontend to send the 'raw' new value. - // Be careful: if frontend sends positive 10 for a consumption, we must flip it? - // Let's assume frontend sends the value exactly as it should be stored. - - // HOWEVER, it's safer if we check type. - const newQuantitySigned = Number(quantity); - - // Validation: Consumption should generally be negative, Entry positive. - // But for flexibility let's just trust the arithmetic diff for now, - // but warn if sign flips unexpectedly? + const item = await StockItem.findOne({ id: movement.stockItemId, organizationId }); + if (!item) return res.status(404).json({ error: 'Stock item not found.' }); const oldQuantity = Number(movement.quantity); - const quantityDiff = newQuantitySigned - oldQuantity; + const quantityDiff = Number(quantity) - oldQuantity; - // Update Item const newStockLevel = Number(item.quantity) + quantityDiff; - if (newStockLevel < 0) { - return res.status(400).json({ error: 'A alteração resultaria em estoque negativo.' }); - } + if (newStockLevel < 0) return res.status(400).json({ error: 'Negative stock results.' }); - item.quantity = newStockLevel; - await item.save(); - - // Audit Log - const typeMap: Record = { ENTRY: 'ENTRADA', CONSUMPTION: 'CONSUMO', ADJUSTMENT: 'AJUSTE' }; - const typeLabel = typeMap[movement.type] || movement.type; + await StockItem.findOneAndUpdate({ id: item.id }, { quantity: newStockLevel }); await StockAuditLog.create({ organizationId, - stockItemId: item._id, - movementId: movement._id, + stockItemId: item.id, + movementId: movement.id, movementNumber: movement.movementNumber, userId, userName, action: 'UPDATE', - details: `Edição de Movimentação (#${movement.movementNumber || '?'} ${typeLabel}): Qtd ${oldQuantity} -> ${newQuantitySigned}`, + details: `Update: ${oldQuantity} -> ${quantity}`, oldValues: { date: movement.date, quantity: movement.quantity, notes: movement.notes }, - newValues: { date, quantity: newQuantitySigned, notes } + newValues: { date, quantity, notes } }); - // Update Movement - movement.quantity = newQuantitySigned; - if (date) movement.date = date; - if (notes !== undefined) movement.notes = notes; - await movement.save(); - - res.json(movement); - + const updated = await StockMovement.findOneAndUpdate({ id }, { quantity, date, notes }); + res.json(updated); } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - console.error('Error updating movement:', error); - res.status(500).json({ error: message }); + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }); } }; @@ -431,72 +323,48 @@ export const deleteStockMovement = async (req: AuthRequest, res: Response) => { const { id } = req.params; const organizationId = req.appUser?.organizationId; const userName = req.appUser?.name || req.appUser?.email || 'Unknown User'; - const userId = req.appUser?.clerkId || 'system'; + const userId = req.appUser?.id || 'system'; const isAdmin = req.appUser?.role === 'admin' || req.appUser?.organizationRole === 'admin'; - if (!isAdmin) { - return res.status(403).json({ error: 'Apenas administradores podem excluir movimentações.' }); - } + if (!isAdmin) return res.status(403).json({ error: 'No admin permissions.' }); - const movement = await StockMovement.findOne({ _id: id, organizationId }); - if (!movement) return res.status(404).json({ error: 'Movimentação não encontrada.' }); + const movement = await StockMovement.findOne({ id, organizationId }); + if (!movement) return res.status(404).json({ error: 'Not found.' }); - const item = await StockItem.findOne({ _id: movement.stockItemId, organizationId }); - if (!item) return res.status(404).json({ error: 'Item de estoque associado não encontrado.' }); + const item = await StockItem.findOne({ id: movement.stockItemId, organizationId }); + if (!item) return res.status(404).json({ error: 'Item associate not found.' }); - // Reverse the effect - // If we delete an Entry (+10), we MUST subtract 10 from Item. - // If we delete a Consumption (-10), we MUST add 10 (subtract -10) to Item. - // So: Item.quantity -= movement.quantity + const newStockLevel = Number(item.quantity) - Number(movement.quantity); + if (newStockLevel < 0) return res.status(400).json({ error: 'Negative stock results.' }); - const reverseQty = Number(movement.quantity); - const newStockLevel = Number(item.quantity) - reverseQty; - - if (newStockLevel < 0) { - return res.status(400).json({ error: 'A exclusão resultaria em estoque negativo.' }); - } - - item.quantity = newStockLevel; - await item.save(); - - // Audit Log - const typeMap: Record = { ENTRY: 'ENTRADA', CONSUMPTION: 'CONSUMO', ADJUSTMENT: 'AJUSTE' }; - const typeLabel = typeMap[movement.type] || movement.type; + await StockItem.findOneAndUpdate({ id: item.id }, { quantity: newStockLevel }); await StockAuditLog.create({ organizationId, - stockItemId: item._id, - movementId: movement._id, + stockItemId: item.id, + movementId: movement.id, movementNumber: movement.movementNumber, userId, userName, action: 'DELETE', - details: `Exclusão de Movimentação (#${movement.movementNumber || '?'} ${typeLabel}): Qtd ${movement.quantity}`, - oldValues: movement.toObject() + details: `Exclusão: Qtd ${movement.quantity}`, + oldValues: movement }); - await StockMovement.deleteOne({ _id: id }); - + await StockMovement.findOneAndDelete({ id }); res.status(204).send(); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - console.error('Error deleting movement:', error); - res.status(500).json({ error: message }); + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }); } }; export const getStockAuditLogs = async (req: AuthRequest, res: Response) => { try { - const { id } = req.params; // StockItem ID + const { id } = req.params; const organizationId = req.appUser?.organizationId; - - const logs = await StockAuditLog.find({ stockItemId: id, organizationId }) - .sort({ timestamp: -1 }); - + const logs = await StockAuditLog.find({ stockItemId: id, organizationId }); res.json(logs); } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - res.status(500).json({ error: message }); + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }); } }; diff --git a/src/server/lib/compat.ts b/src/server/lib/compat.ts index 2dd31f3..756c693 100644 --- a/src/server/lib/compat.ts +++ b/src/server/lib/compat.ts @@ -1,44 +1,136 @@ import { supabase, findOneGpi, queryGpi, insertGpi, updateGpi, deleteGpi } from '../config/supabase.js'; +/** + * Mongoose Compatibility Layer for Supabase (v2025) + * Translates Mongoose-style calls to Supabase PostgREST queries. + * Automatically handles camelCase (JS) to snake_case (DB) mapping for core fields. + */ function createModel(tableName: string) { + const mapToDb = (data: any) => { + const mapped: any = { ...data }; + if (mapped.organizationId) { mapped.organization_id = mapped.organizationId; delete mapped.organizationId; } + if (mapped.projectId) { mapped.project_id = mapped.projectId; delete mapped.projectId; } + if (mapped.createdBy) { mapped.created_by = mapped.createdBy; delete mapped.createdBy; } + if (mapped.createdAt) { mapped.created_at = mapped.createdAt; delete mapped.createdAt; } + if (mapped.updatedAt) { mapped.updated_at = mapped.updatedAt; delete mapped.updatedAt; } + return mapped; + }; + + const mapFromDb = (data: any) => { + if (!data) return data; + if (Array.isArray(data)) return data.map(mapFromDb); + const mapped: any = { ...data, id: data.id || data._id }; + if (mapped.organization_id) mapped.organizationId = mapped.organization_id; + if (mapped.project_id) mapped.projectId = mapped.project_id; + if (mapped.created_by) mapped.createdBy = mapped.created_by; + return mapped; + }; + return { - find: async (query: any = {}) => { - const { data, error } = await queryGpi(tableName, { filter: query }); - if (error) throw error; - return data || []; + find: function(query: any = {}) { + const dbQuery = mapToDb(query); + const promise = (async () => { + const { data, error } = await queryGpi(tableName, { filter: dbQuery }); + if (error) throw error; + return mapFromDb(data || []); + })(); + + // Mock methods for chainability + (promise as any).sort = () => promise; + (promise as any).populate = () => promise; + (promise as any).lean = () => promise; + (promise as any).limit = () => promise; + (promise as any).select = () => promise; + + return promise; }, findOne: async (query: any) => { - return await findOneGpi(tableName, query); + const data = await findOneGpi(tableName, mapToDb(query)); + return mapFromDb(data); }, findById: async (id: string) => { - return await findOneGpi(tableName, { id }); + const data = await findOneGpi(tableName, { id }); + return mapFromDb(data); }, create: async (data: any) => { - const result = await insertGpi(tableName, data); - return result.data?.[0] || result.data; + const dbData = mapToDb(data); + const result = await insertGpi(tableName, dbData); + return mapFromDb(result.data?.[0] || result.data); + }, + insertMany: async (dataArray: any[]) => { + const dbDataArray = dataArray.map(mapToDb); + const { data, error } = await supabase.from(tableName).insert(dbDataArray).select(); + if (error) throw error; + return mapFromDb(data); + }, + deleteMany: async (query: any) => { + const dbQuery = mapToDb(query); + let q = supabase.from(tableName).delete(); + Object.keys(dbQuery).forEach(key => { + q = q.eq(key, dbQuery[key]); + }); + const { error } = await q; + if (error) throw error; + return { deletedCount: 0 }; // Supabase doesn't return count easily here + }, + countDocuments: async (query: any = {}) => { + const dbQuery = mapToDb(query); + let q = supabase.from(tableName).select('*', { count: 'exact', head: true }); + Object.keys(dbQuery).forEach(key => { + q = q.eq(key, dbQuery[key]); + }); + const { count, error } = await q; + if (error) throw error; + return count || 0; }, - save: async function() { return this; }, findOneAndUpdate: async (query: any, update: any) => { - const existing = await findOneGpi(tableName, query); + const existing = await findOneGpi(tableName, mapToDb(query)); if (!existing) return null; - const result = await updateGpi(tableName, existing.id, update); - return result.data?.[0]; + const result = await updateGpi(tableName, existing.id, mapToDb(update)); + return mapFromDb(result.data?.[0]); }, findByIdAndUpdate: async (id: string, update: any) => { - const result = await updateGpi(tableName, id, update); - return result.data?.[0]; - }, - findOneAndDelete: async (query: any) => { - const existing = await findOneGpi(tableName, query); - if (!existing) return null; - await deleteGpi(tableName, existing.id); - return existing; + const result = await updateGpi(tableName, id, mapToDb(update)); + return mapFromDb(result.data?.[0]); }, findByIdAndDelete: async (id: string) => { await deleteGpi(tableName, id); return { id }; }, - aggregate: (pipeline: any[]) => ({ toArray: async () => [] }) + findOneAndDelete: async (query: any) => { + const dbQuery = mapToDb(query); + const existing = await findOneGpi(tableName, dbQuery); + if (!existing) return null; + await deleteGpi(tableName, existing.id); + return mapFromDb(existing); + }, + updateMany: async (query: any, update: any) => { + const dbQuery = mapToDb(query); + const dbUpdate = mapToDb(update.$set || update); + let q = supabase.from(tableName).update(dbUpdate); + Object.keys(dbQuery).forEach(key => { + q = q.eq(key, dbQuery[key]); + }); + const { error } = await q; + if (error) throw error; + return { acknowledged: true, modifiedCount: 0 }; + }, + deleteOne: async (query: any) => { + const existing = await findOneGpi(tableName, mapToDb(query)); + if (!existing) return null; + await deleteGpi(tableName, existing.id); + return existing; + }, + aggregate: (pipeline: any[]) => ({ toArray: async () => [] }), + // For "new Model()" usage + new: function(data: any) { + const instance = { ...data }; + (instance as any).save = async () => { + return await insertGpi(tableName, mapToDb(instance)); + }; + (instance as any).toObject = () => instance; + return instance; + } }; } @@ -64,4 +156,4 @@ export const StoredFile = createModel('stored_files'); export { queryGpi, findOneGpi, insertGpi, updateGpi, deleteGpi }; -console.log('✅ Mongoose Compatibility Layer loaded'); +console.log('✅ Mongoose Compatibility Layer load complete (Extended Mode)'); diff --git a/src/server/services/backupService.ts b/src/server/services/backupService.ts index 4244e02..64ac736 100644 --- a/src/server/services/backupService.ts +++ b/src/server/services/backupService.ts @@ -1,14 +1,8 @@ -import Project from '../models/Project.js'; -import Inspection from '../models/Inspection.js'; -import ApplicationRecord from '../models/ApplicationRecord.js'; -import TechnicalDataSheet from '../models/TechnicalDataSheet.js'; -import PaintingScheme from '../models/PaintingScheme.js'; -import Part from '../models/Part.js'; -import Instrument from '../models/Instrument.js'; -import YieldStudy from '../models/YieldStudy.js'; -import GeometryType from '../models/GeometryType.js'; -import StockItem from '../models/StockItem.js'; -import StockMovement from '../models/StockMovement.js'; +import { + Project, Inspection, ApplicationRecord, TechnicalDataSheet, PaintingScheme, + Part, Instrument, YieldStudy, GeometryType, StockItem, StockMovement +} from '../lib/compat.js'; + interface BackupData { version: string; @@ -48,17 +42,17 @@ export const backupService = { stockItems, stockMovements ] = await Promise.all([ - Project.find({ organizationId }).lean(), - Inspection.find({ organizationId }).lean(), - ApplicationRecord.find({ organizationId }).lean(), - TechnicalDataSheet.find({ organizationId }).lean(), - PaintingScheme.find({ organizationId }).lean(), - Part.find({ organizationId }).lean(), - Instrument.find({ organizationId }).lean(), - YieldStudy.find({ organizationId }).lean(), - GeometryType.find({ organizationId }).lean(), - StockItem.find({ organizationId }).lean(), - StockMovement.find({ organizationId }).lean() + Project.find({ organizationId }), + Inspection.find({ organizationId }), + ApplicationRecord.find({ organizationId }), + TechnicalDataSheet.find({ organizationId }), + PaintingScheme.find({ organizationId }), + Part.find({ organizationId }), + Instrument.find({ organizationId }), + YieldStudy.find({ organizationId }), + GeometryType.find({ organizationId }), + StockItem.find({ organizationId }), + StockMovement.find({ organizationId }) ]); const backup: BackupData = { diff --git a/src/server/services/yieldStudyService.ts b/src/server/services/yieldStudyService.ts index 337680c..15c43ec 100644 --- a/src/server/services/yieldStudyService.ts +++ b/src/server/services/yieldStudyService.ts @@ -1,25 +1,28 @@ -import YieldStudy from '../models/YieldStudy.js'; +import { YieldStudy, TechnicalDataSheet } from '../lib/compat.js'; export const getAllStudies = async (organizationId?: string) => { const query = organizationId ? { organizationId } : {}; - const studies = await YieldStudy.find(query).populate('dataSheetId').sort({ createdAt: -1 }).lean(); - return studies.map(s => ({ ...s, id: s._id.toString() })); + // Compatibility layer handles .find() and returns plain objects with id + const studies = await YieldStudy.find(query); + + // Attempt manual population if dataSheetId exists + return await Promise.all(studies.map(async (s: any) => { + if (s.dataSheetId) { + const dataSheet = await TechnicalDataSheet.findById(s.dataSheetId); + return { ...s, dataSheetId: dataSheet || s.dataSheetId }; + } + return s; + })); }; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const createStudy = async (data: any & { organizationId?: string }) => { - const newStudy = new YieldStudy({ ...data, organizationId: data.organizationId }); - const saved = await newStudy.save(); - return { ...saved.toObject(), id: saved._id.toString() }; + const saved = await YieldStudy.create({ ...data, organizationId: data.organizationId }); + return saved; }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any export const updateStudy = async (id: string, updates: any, organizationId?: string) => { - // SECURITY FIX: Allow update if: - // 1. Matches ID AND Matches Organization - // 2. OR Matches ID AND Record has NO organization (legacy/orphan) -> Adopt it! - const existing = await YieldStudy.findById(id); if (!existing) return null; @@ -32,15 +35,11 @@ export const updateStudy = async (id: string, updates: any, organizationId?: str updates.organizationId = organizationId; } - const updated = await YieldStudy.findOneAndUpdate({ _id: id }, updates, { new: true }).lean(); - if (updated) { - return { ...updated, id: updated._id.toString() }; - } - return null; + const updated = await YieldStudy.findOneAndUpdate({ id }, updates); + return updated; }; export const deleteStudy = async (id: string, organizationId?: string) => { - // SECURITY FIX: Same logic as update - allow delete if owned OR if orphan const existing = await YieldStudy.findById(id); if (!existing) return false; @@ -52,5 +51,3 @@ export const deleteStudy = async (id: string, organizationId?: string) => { await YieldStudy.findByIdAndDelete(id); return true; }; - -