🚀 Auto-deploy: GPI atualizado em 02/04/2026 01:07:29
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import GeometryType from '../models/GeometryType.js';
|
import { GeometryType } from '../lib/compat.js';
|
||||||
|
|
||||||
import { IAppUser } from '../middleware/authMiddleware.js';
|
import { IAppUser } from '../middleware/authMiddleware.js';
|
||||||
|
|
||||||
interface AuthRequest extends Request {
|
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)
|
// 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)
|
// Auto-seed if empty AND we HAVE an organization (don't seed for global view)
|
||||||
if (types.length === 0 && organizationId) {
|
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' });
|
return res.status(400).json({ error: 'Name is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const newType = new GeometryType({
|
const saved = await GeometryType.create({
|
||||||
name,
|
name,
|
||||||
efficiencyLoss: Number(efficiencyLoss) || 0,
|
efficiencyLoss: Number(efficiencyLoss) || 0,
|
||||||
organizationId
|
organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
const saved = await newType.save();
|
|
||||||
res.status(201).json(saved);
|
res.status(201).json(saved);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
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 isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com';
|
||||||
const { name, efficiencyLoss } = req.body;
|
const { name, efficiencyLoss } = req.body;
|
||||||
|
|
||||||
const query = isGlobalAdmin
|
|
||||||
? { _id: id }
|
|
||||||
: { _id: id, organizationId };
|
|
||||||
|
|
||||||
const updated = await GeometryType.findOneAndUpdate(
|
const updated = await GeometryType.findOneAndUpdate(
|
||||||
query,
|
{ id, organizationId },
|
||||||
{ name, efficiencyLoss: Number(efficiencyLoss) },
|
{ name, efficiencyLoss: Number(efficiencyLoss) }
|
||||||
{ new: true }
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!updated) {
|
if (!updated) {
|
||||||
@@ -142,11 +136,7 @@ export const deleteType = async (req: AuthRequest, res: Response) => {
|
|||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com';
|
const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com';
|
||||||
|
|
||||||
const query = isGlobalAdmin
|
const deleted = await GeometryType.findOneAndDelete({ id, organizationId });
|
||||||
? { _id: id }
|
|
||||||
: { _id: id, organizationId };
|
|
||||||
|
|
||||||
const deleted = await GeometryType.findOneAndDelete(query);
|
|
||||||
|
|
||||||
if (!deleted) {
|
if (!deleted) {
|
||||||
return res.status(404).json({ error: 'Record not found' });
|
return res.status(404).json({ error: 'Record not found' });
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import Instrument from '../models/Instrument.js';
|
import { Instrument } from '../lib/compat.js';
|
||||||
|
|
||||||
import { IAppUser } from '../middleware/authMiddleware.js';
|
import { IAppUser } from '../middleware/authMiddleware.js';
|
||||||
|
|
||||||
interface AuthRequest extends Request {
|
interface AuthRequest extends Request {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import Message from '../models/Message.js';
|
import { Message, OrganizationMember } from '../lib/compat.js';
|
||||||
import OrganizationMember from '../models/OrganizationMember.js';
|
|
||||||
|
|
||||||
// Send a message
|
// Send a message
|
||||||
export const sendMessage = async (req: Request, res: Response) => {
|
export const sendMessage = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { toUserId, message } = req.body;
|
const { toUserId, message } = req.body;
|
||||||
const fromUserId = req.appUser?.clerkId;
|
const fromUserId = req.appUser?.id;
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
if (!organizationId) {
|
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.' });
|
return res.status(400).json({ error: 'Destinatário e mensagem são obrigatórios.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.length > 255) {
|
// Check if there's already a pending (unread) message
|
||||||
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
|
|
||||||
const existingMessage = await Message.findOne({
|
const existingMessage = await Message.findOne({
|
||||||
organizationId,
|
organizationId,
|
||||||
fromUserId,
|
fromUserId,
|
||||||
@@ -34,22 +29,21 @@ export const sendMessage = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (existingMessage) {
|
if (existingMessage) {
|
||||||
// Update existing message instead of creating a new one
|
const updated = await Message.findOneAndUpdate(
|
||||||
existingMessage.message = message;
|
{ id: existingMessage.id },
|
||||||
existingMessage.updatedAt = new Date();
|
{ message, updatedAt: new Date() }
|
||||||
await existingMessage.save();
|
);
|
||||||
return res.json(existingMessage);
|
return res.json(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new message
|
// Create new message
|
||||||
const newMessage = new Message({
|
const newMessage = await Message.create({
|
||||||
organizationId,
|
organizationId,
|
||||||
fromUserId,
|
fromUserId,
|
||||||
toUserId,
|
toUserId,
|
||||||
message,
|
message,
|
||||||
});
|
});
|
||||||
|
|
||||||
await newMessage.save();
|
|
||||||
res.status(201).json(newMessage);
|
res.status(201).json(newMessage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending message:', 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
|
// Get unread messages for current user
|
||||||
export const getUnreadMessages = async (req: Request, res: Response) => {
|
export const getUnreadMessages = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const toUserId = req.appUser?.clerkId;
|
const toUserId = req.appUser?.id;
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
@@ -77,14 +71,14 @@ export const getUnreadMessages = async (req: Request, res: Response) => {
|
|||||||
isRead: false,
|
isRead: false,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
isDeletedByRecipient: false,
|
isDeletedByRecipient: false,
|
||||||
}).sort({ createdAt: -1 });
|
});
|
||||||
|
|
||||||
// Populate sender info
|
// Populate sender info
|
||||||
const messagesWithSender = await Promise.all(
|
const messagesWithSender = await Promise.all(
|
||||||
messages.map(async (msg) => {
|
messages.map(async (msg: any) => {
|
||||||
const sender = await OrganizationMember.findOne({ clerkUserId: msg.fromUserId });
|
const sender = await OrganizationMember.findOne({ userId: msg.fromUserId });
|
||||||
return {
|
return {
|
||||||
...msg.toObject(),
|
...msg,
|
||||||
fromUser: sender ? { name: sender.name, email: sender.email } : null,
|
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) => {
|
export const markMessageAsRead = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const userId = req.appUser?.clerkId;
|
const userId = req.appUser?.id;
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
if (!organizationId) {
|
if (!userId) return res.status(401).json({ error: 'Usuário não autenticado.' });
|
||||||
return res.status(400).json({ error: 'Organização não selecionada.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!userId) {
|
const updated = await Message.findOneAndUpdate(
|
||||||
return res.status(401).json({ error: 'Usuário não autenticado.' });
|
{ id, organizationId, toUserId: userId },
|
||||||
}
|
{ isRead: true, readAt: new Date() }
|
||||||
|
);
|
||||||
|
|
||||||
const message = await Message.findOne({
|
if (!updated) {
|
||||||
_id: id,
|
|
||||||
organizationId,
|
|
||||||
toUserId: userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!message) {
|
|
||||||
return res.status(404).json({ error: 'Mensagem não encontrada.' });
|
return res.status(404).json({ error: 'Mensagem não encontrada.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
message.isRead = true;
|
res.json(updated);
|
||||||
message.readAt = new Date();
|
|
||||||
await message.save();
|
|
||||||
|
|
||||||
res.json(message);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error marking message as read:', error);
|
console.error('Error marking message as read:', error);
|
||||||
res.status(500).json({ error: 'Erro ao marcar mensagem como lida.' });
|
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
|
// Get my pending (unread) sent messages
|
||||||
export const getMyPendingMessages = async (req: Request, res: Response) => {
|
export const getMyPendingMessages = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const fromUserId = req.appUser?.clerkId;
|
const fromUserId = req.appUser?.id;
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
if (!organizationId) {
|
if (!fromUserId) return res.status(401).json({ error: 'Usuário não autenticado.' });
|
||||||
return res.status(400).json({ error: 'Organização não selecionada.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fromUserId) {
|
|
||||||
return res.status(401).json({ error: 'Usuário não autenticado.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const messages = await Message.find({
|
const messages = await Message.find({
|
||||||
organizationId,
|
organizationId,
|
||||||
fromUserId,
|
fromUserId,
|
||||||
isRead: false,
|
isRead: false,
|
||||||
}).sort({ createdAt: -1 });
|
});
|
||||||
|
|
||||||
// Populate recipient info
|
// Populate recipient info
|
||||||
const messagesWithRecipient = await Promise.all(
|
const messagesWithRecipient = await Promise.all(
|
||||||
messages.map(async (msg) => {
|
messages.map(async (msg: any) => {
|
||||||
const recipient = await OrganizationMember.findOne({ clerkUserId: msg.toUserId });
|
const recipient = await OrganizationMember.findOne({ userId: msg.toUserId });
|
||||||
return {
|
return {
|
||||||
...msg.toObject(),
|
...msg,
|
||||||
toUser: recipient ? { name: recipient.name, email: recipient.email } : null,
|
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) => {
|
export const deleteMessage = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const userId = req.appUser?.clerkId;
|
const userId = req.appUser?.id;
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
if (!organizationId) {
|
const deleted = await Message.findOneAndDelete({
|
||||||
return res.status(400).json({ error: 'Organização não selecionada.' });
|
id,
|
||||||
}
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
return res.status(401).json({ error: 'Usuário não autenticado.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = await Message.findOne({
|
|
||||||
_id: id,
|
|
||||||
organizationId,
|
organizationId,
|
||||||
fromUserId: userId,
|
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.' });
|
return res.status(404).json({ error: 'Mensagem não encontrada ou já foi lida.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
await message.deleteOne();
|
res.status(204).send();
|
||||||
res.json({ message: 'Mensagem deletada com sucesso.' });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting message:', error);
|
console.error('Error deleting message:', error);
|
||||||
res.status(500).json({ error: 'Erro ao deletar mensagem.' });
|
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) => {
|
export const archiveMessage = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const userId = req.appUser?.clerkId;
|
const userId = req.appUser?.id;
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
const message = await Message.findOne({ _id: id, toUserId: userId, organizationId });
|
const updated = await Message.findOneAndUpdate(
|
||||||
if (!message) return res.status(404).json({ error: 'Mensagem não encontrada.' });
|
{ id, toUserId: userId, organizationId },
|
||||||
|
{ isArchived: true, isRead: true }
|
||||||
|
);
|
||||||
|
|
||||||
message.isArchived = true;
|
if (!updated) return res.status(404).json({ error: 'Mensagem não encontrada.' });
|
||||||
message.isRead = true; // Arquivar implica ler
|
res.json(updated);
|
||||||
await message.save();
|
|
||||||
res.json(message);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error archiving message:', error);
|
console.error('Error archiving message:', error);
|
||||||
res.status(500).json({ error: 'Erro ao arquivar mensagem.' });
|
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) => {
|
export const recipientDeleteMessage = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const userId = req.appUser?.clerkId;
|
const userId = req.appUser?.id;
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
const message = await Message.findOne({ _id: id, toUserId: userId, organizationId });
|
const updated = await Message.findOneAndUpdate(
|
||||||
if (!message) return res.status(404).json({ error: 'Mensagem não encontrada.' });
|
{ id, toUserId: userId, organizationId },
|
||||||
|
{ isDeletedByRecipient: true }
|
||||||
|
);
|
||||||
|
|
||||||
message.isDeletedByRecipient = true;
|
if (!updated) return res.status(404).json({ error: 'Mensagem não encontrada.' });
|
||||||
await message.save();
|
|
||||||
res.json({ message: 'Mensagem excluída com sucesso.' });
|
res.json({ message: 'Mensagem excluída com sucesso.' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting message:', error);
|
console.error('Error deleting message:', error);
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import StockItem from '../models/StockItem.js';
|
import { StockItem, StockMovement, StockAuditLog, TechnicalDataSheet } from '../lib/compat.js';
|
||||||
import StockMovement from '../models/StockMovement.js';
|
|
||||||
|
|
||||||
import { IAppUser } from '../middleware/authMiddleware.js';
|
import { IAppUser } from '../middleware/authMiddleware.js';
|
||||||
import { notificationService } from '../services/notificationService.js';
|
import { notificationService } from '../services/notificationService.js';
|
||||||
|
|
||||||
@@ -27,47 +25,33 @@ export const createStockItem = async (req: AuthRequest, res: Response) => {
|
|||||||
minStock
|
minStock
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
// Validation
|
|
||||||
if (!dataSheetId || !rrNumber || !batchNumber || quantity === undefined || !unit) {
|
if (!dataSheetId || !rrNumber || !batchNumber || quantity === undefined || !unit) {
|
||||||
return res.status(400).json({ error: 'Campos obrigatórios: DataSheet, RR, Lote, Quantidade, Unidade.' });
|
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 });
|
const existing = await StockItem.findOne({ organizationId, rrNumber });
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return res.status(400).json({ error: `Já existe um item com o RR ${rrNumber}.` });
|
return res.status(400).json({ error: `Já existe um item com o RR ${rrNumber}.` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Min Stock Inheritance Logic ---
|
|
||||||
let finalMinStock = Number(minStock) || 0;
|
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) {
|
if (finalMinStock === 0) {
|
||||||
const existingGroupItem = await StockItem.findOne({
|
const items = await StockItem.find({ organizationId, dataSheetId, color });
|
||||||
organizationId,
|
if (items.length > 0) {
|
||||||
dataSheetId,
|
finalMinStock = items[0].minStock || 0;
|
||||||
color
|
|
||||||
}).sort({ updatedAt: -1 }); // Get latest active config
|
|
||||||
|
|
||||||
if (existingGroupItem && existingGroupItem.minStock > 0) {
|
|
||||||
finalMinStock = existingGroupItem.minStock;
|
|
||||||
}
|
}
|
||||||
} else {
|
} 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) {
|
if (finalMinStock > 0) {
|
||||||
await StockItem.updateMany(
|
await StockItem.updateMany(
|
||||||
{ organizationId, dataSheetId, color },
|
{ organizationId, dataSheetId, color },
|
||||||
{ $set: { minStock: finalMinStock } }
|
{ minStock: finalMinStock }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newItem = new StockItem({
|
const savedItem = await StockItem.create({
|
||||||
organizationId,
|
organizationId,
|
||||||
createdBy: req.appUser?.clerkId,
|
createdBy: req.appUser?.id,
|
||||||
dataSheetId,
|
dataSheetId,
|
||||||
rrNumber,
|
rrNumber,
|
||||||
batchNumber,
|
batchNumber,
|
||||||
@@ -81,13 +65,10 @@ export const createStockItem = async (req: AuthRequest, res: Response) => {
|
|||||||
receivedBy
|
receivedBy
|
||||||
});
|
});
|
||||||
|
|
||||||
const savedItem = await newItem.save();
|
|
||||||
|
|
||||||
// Create Initial Movement (ENTRY)
|
|
||||||
await StockMovement.create({
|
await StockMovement.create({
|
||||||
organizationId,
|
organizationId,
|
||||||
createdBy: req.appUser?.clerkId,
|
createdBy: req.appUser?.id,
|
||||||
stockItemId: savedItem._id,
|
stockItemId: savedItem.id,
|
||||||
movementNumber: 1,
|
movementNumber: 1,
|
||||||
type: 'ENTRY',
|
type: 'ENTRY',
|
||||||
quantity: Number(quantity),
|
quantity: Number(quantity),
|
||||||
@@ -95,25 +76,21 @@ export const createStockItem = async (req: AuthRequest, res: Response) => {
|
|||||||
notes: 'Abertura de Lote / Entrada Inicial'
|
notes: 'Abertura de Lote / Entrada Inicial'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Notificação de Recebimento
|
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
await notificationService.create({
|
await notificationService.create({
|
||||||
organizationId,
|
organizationId,
|
||||||
title: 'Recebimento de Material',
|
title: 'Recebimento de Material',
|
||||||
message: `Recebido: ${quantity}${unit} de ${savedItem.rrNumber} (Lote: ${batchNumber}).`,
|
message: `Recebido: ${quantity}${unit} de ${savedItem.rrNumber} (Lote: ${batchNumber}).`,
|
||||||
type: 'info',
|
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);
|
res.status(201).json(savedItem);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
console.error('Error creating stock item:', 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 {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
// Only allow updating metadata, NOT quantity directly (quantity must be via adjustments)
|
const { quantity, ...otherData } = req.body;
|
||||||
// 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
|
|
||||||
|
|
||||||
if (quantity !== undefined) {
|
if (quantity !== undefined) {
|
||||||
return res.status(400).json({ error: 'Para alterar a quantidade, utilize as funções de Ajuste ou Consumo.' });
|
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) {
|
if (otherData.minStock !== undefined) {
|
||||||
const item = await StockItem.findOne({ _id: id, organizationId });
|
const item = await StockItem.findOne({ id, organizationId });
|
||||||
if (item) {
|
if (item) {
|
||||||
// Propagate to all siblings (same Product + Color)
|
|
||||||
await StockItem.updateMany(
|
await StockItem.updateMany(
|
||||||
{
|
{ organizationId, dataSheetId: item.dataSheetId, color: item.color },
|
||||||
organizationId,
|
{ minStock: otherData.minStock }
|
||||||
dataSheetId: item.dataSheetId,
|
|
||||||
color: item.color
|
|
||||||
},
|
|
||||||
{ $set: { minStock: otherData.minStock } }
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = await StockItem.findOneAndUpdate(
|
const updated = await StockItem.findOneAndUpdate({ id, organizationId }, otherData);
|
||||||
{ _id: id, organizationId },
|
|
||||||
otherData,
|
|
||||||
{ new: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!updated) return res.status(404).json({ error: 'Item não encontrado.' });
|
if (!updated) return res.status(404).json({ error: 'Item não encontrado.' });
|
||||||
|
|
||||||
// Check Low Stock (in case minStock changed)
|
await notificationService.checkLowStock(id);
|
||||||
await notificationService.checkLowStock(updated._id.toString());
|
|
||||||
|
|
||||||
res.json(updated);
|
res.json(updated);
|
||||||
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
|
||||||
res.status(500).json({ error: message });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -172,46 +129,36 @@ export const adjustStock = async (req: AuthRequest, res: Response) => {
|
|||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const userName = req.appUser?.name || req.appUser?.email || 'Unknown User';
|
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 (!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.' });
|
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.' });
|
if (!item) return res.status(404).json({ error: 'Item não encontrado.' });
|
||||||
|
|
||||||
// Calculate new quantity
|
|
||||||
const newQuantity = Number(item.quantity) + Number(quantityDelta);
|
const newQuantity = Number(item.quantity) + Number(quantityDelta);
|
||||||
if (newQuantity < 0) return res.status(400).json({ error: 'Estoque insuficiente para este ajuste.' });
|
if (newQuantity < 0) return res.status(400).json({ error: 'Estoque insuficiente para este ajuste.' });
|
||||||
|
|
||||||
item.quantity = newQuantity;
|
await StockItem.findOneAndUpdate({ id }, { quantity: newQuantity });
|
||||||
await item.save();
|
|
||||||
|
|
||||||
// Calculate next movement number
|
const count = await StockMovement.countDocuments({ stockItemId: id });
|
||||||
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
|
|
||||||
await StockMovement.create({
|
await StockMovement.create({
|
||||||
organizationId,
|
organizationId,
|
||||||
createdBy: req.appUser?.clerkId,
|
createdBy: req.appUser?.id,
|
||||||
stockItemId: item._id,
|
stockItemId: id,
|
||||||
movementNumber,
|
movementNumber: count + 1,
|
||||||
type: 'ADJUSTMENT',
|
type: 'ADJUSTMENT',
|
||||||
quantity: Number(quantityDelta),
|
quantity: Number(quantityDelta),
|
||||||
responsible: userName,
|
responsible: userName,
|
||||||
reason
|
reason
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check Low Stock
|
await notificationService.checkLowStock(id);
|
||||||
await notificationService.checkLowStock(item._id.toString());
|
res.json({ ...item, quantity: newQuantity });
|
||||||
|
|
||||||
res.json(item);
|
|
||||||
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
|
||||||
res.status(500).json({ error: message });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -225,40 +172,32 @@ export const consumeStock = async (req: AuthRequest, res: Response) => {
|
|||||||
if (!requester) return res.status(400).json({ error: 'Solicitante é obrigatório.' });
|
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.' });
|
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) return res.status(404).json({ error: 'Item não encontrado.' });
|
||||||
|
|
||||||
if (item.quantity < Number(quantityConsumed)) return res.status(400).json({ error: 'Estoque insuficiente.' });
|
if (item.quantity < Number(quantityConsumed)) return res.status(400).json({ error: 'Estoque insuficiente.' });
|
||||||
|
|
||||||
item.quantity -= Number(quantityConsumed);
|
const newQuantity = Number(item.quantity) - Number(quantityConsumed);
|
||||||
await item.save();
|
await StockItem.findOneAndUpdate({ id }, { quantity: newQuantity });
|
||||||
|
|
||||||
// Calculate next movement number
|
const count = await StockMovement.countDocuments({ stockItemId: id });
|
||||||
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 (Negative quantity for consumption)
|
|
||||||
await StockMovement.create({
|
await StockMovement.create({
|
||||||
organizationId,
|
organizationId,
|
||||||
createdBy: req.appUser?.clerkId,
|
createdBy: req.appUser?.id,
|
||||||
stockItemId: item._id,
|
stockItemId: id,
|
||||||
movementNumber,
|
movementNumber: count + 1,
|
||||||
type: 'CONSUMPTION',
|
type: 'CONSUMPTION',
|
||||||
quantity: -Number(quantityConsumed), // Negative
|
quantity: -Number(quantityConsumed),
|
||||||
responsible: userName,
|
responsible: userName,
|
||||||
requester,
|
requester,
|
||||||
date: date || new Date()
|
date: date || new Date()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check Low Stock
|
await notificationService.checkLowStock(id);
|
||||||
await notificationService.checkLowStock(item._id.toString());
|
res.json({ ...item, quantity: newQuantity });
|
||||||
|
|
||||||
res.json(item);
|
|
||||||
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
|
||||||
res.status(500).json({ error: message });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -267,20 +206,15 @@ export const deleteStockItem = async (req: AuthRequest, res: Response) => {
|
|||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
|
|
||||||
// Optional: Block delete if there are movements other than ENTRY?
|
const deleted = await StockItem.findOneAndDelete({ id, organizationId });
|
||||||
// For simplicity allow Admin to nuke it.
|
|
||||||
|
|
||||||
const deleted = await StockItem.findOneAndDelete({ _id: id, organizationId });
|
|
||||||
if (!deleted) return res.status(404).json({ error: 'Item não encontrado.' });
|
if (!deleted) return res.status(404).json({ error: 'Item não encontrado.' });
|
||||||
|
|
||||||
// Cleanup movements & logs
|
|
||||||
await StockMovement.deleteMany({ stockItemId: id });
|
await StockMovement.deleteMany({ stockItemId: id });
|
||||||
await StockAuditLog.deleteMany({ stockItemId: id });
|
await StockAuditLog.deleteMany({ stockItemId: id });
|
||||||
|
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
|
||||||
res.status(500).json({ error: message });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -289,19 +223,23 @@ export const getStockItems = async (req: AuthRequest, res: Response) => {
|
|||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const { dataSheetId } = req.query;
|
const { dataSheetId } = req.query;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const query: any = { organizationId };
|
const query: any = { organizationId };
|
||||||
if (dataSheetId) query.dataSheetId = dataSheetId;
|
if (dataSheetId) query.dataSheetId = dataSheetId;
|
||||||
|
|
||||||
// Sort by Expiration Date ASC (First to expire first)
|
const items = await StockItem.find(query);
|
||||||
const items = await StockItem.find(query)
|
|
||||||
.populate('dataSheetId', 'name manufacturer type')
|
|
||||||
.sort({ expirationDate: 1 });
|
|
||||||
|
|
||||||
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) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
|
||||||
res.status(500).json({ error: message });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -310,119 +248,73 @@ export const getStockItemById = async (req: AuthRequest, res: Response) => {
|
|||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
|
|
||||||
const item = await StockItem.findOne({ _id: id, organizationId })
|
const item = await StockItem.findOne({ id, organizationId });
|
||||||
.populate('dataSheetId', 'name manufacturer type');
|
|
||||||
|
|
||||||
if (!item) return res.status(404).json({ error: 'Item não encontrado.' });
|
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);
|
res.json(item);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
|
||||||
res.status(500).json({ error: message });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStockMovements = async (req: AuthRequest, res: Response) => {
|
export const getStockMovements = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params; // StockItem ID
|
const { id } = req.params;
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
|
const movements = await StockMovement.find({ stockItemId: id, organizationId });
|
||||||
const movements = await StockMovement.find({ stockItemId: id, organizationId })
|
|
||||||
.sort({ date: -1 });
|
|
||||||
|
|
||||||
res.json(movements);
|
res.json(movements);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
|
||||||
res.status(500).json({ error: message });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
// CRUD & Auditing for Movements
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
|
|
||||||
import StockAuditLog from '../models/StockAuditLog.js';
|
|
||||||
|
|
||||||
export const updateStockMovement = async (req: AuthRequest, res: Response) => {
|
export const updateStockMovement = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params; // Movement ID
|
const { id } = req.params;
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const userName = req.appUser?.name || req.appUser?.email || 'Unknown User';
|
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';
|
const isAdmin = req.appUser?.role === 'admin' || req.appUser?.organizationRole === 'admin';
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) return res.status(403).json({ error: 'No admin permissions.' });
|
||||||
return res.status(403).json({ error: 'Apenas administradores podem editar movimentações.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { date, quantity, notes } = req.body;
|
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 });
|
const item = await StockItem.findOne({ id: movement.stockItemId, organizationId });
|
||||||
if (!movement) return res.status(404).json({ error: 'Movimentação não encontrada.' });
|
if (!item) return res.status(404).json({ error: 'Stock item 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.' });
|
|
||||||
|
|
||||||
// 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 oldQuantity = Number(movement.quantity);
|
const oldQuantity = Number(movement.quantity);
|
||||||
const quantityDiff = newQuantitySigned - oldQuantity;
|
const quantityDiff = Number(quantity) - oldQuantity;
|
||||||
|
|
||||||
// Update Item
|
|
||||||
const newStockLevel = Number(item.quantity) + quantityDiff;
|
const newStockLevel = Number(item.quantity) + quantityDiff;
|
||||||
if (newStockLevel < 0) {
|
if (newStockLevel < 0) return res.status(400).json({ error: 'Negative stock results.' });
|
||||||
return res.status(400).json({ error: 'A alteração resultaria em estoque negativo.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
item.quantity = newStockLevel;
|
await StockItem.findOneAndUpdate({ id: item.id }, { quantity: newStockLevel });
|
||||||
await item.save();
|
|
||||||
|
|
||||||
// Audit Log
|
|
||||||
const typeMap: Record<string, string> = { ENTRY: 'ENTRADA', CONSUMPTION: 'CONSUMO', ADJUSTMENT: 'AJUSTE' };
|
|
||||||
const typeLabel = typeMap[movement.type] || movement.type;
|
|
||||||
|
|
||||||
await StockAuditLog.create({
|
await StockAuditLog.create({
|
||||||
organizationId,
|
organizationId,
|
||||||
stockItemId: item._id,
|
stockItemId: item.id,
|
||||||
movementId: movement._id,
|
movementId: movement.id,
|
||||||
movementNumber: movement.movementNumber,
|
movementNumber: movement.movementNumber,
|
||||||
userId,
|
userId,
|
||||||
userName,
|
userName,
|
||||||
action: 'UPDATE',
|
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 },
|
oldValues: { date: movement.date, quantity: movement.quantity, notes: movement.notes },
|
||||||
newValues: { date, quantity: newQuantitySigned, notes }
|
newValues: { date, quantity, notes }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update Movement
|
const updated = await StockMovement.findOneAndUpdate({ id }, { quantity, date, notes });
|
||||||
movement.quantity = newQuantitySigned;
|
res.json(updated);
|
||||||
if (date) movement.date = date;
|
|
||||||
if (notes !== undefined) movement.notes = notes;
|
|
||||||
await movement.save();
|
|
||||||
|
|
||||||
res.json(movement);
|
|
||||||
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
|
||||||
console.error('Error updating movement:', error);
|
|
||||||
res.status(500).json({ error: message });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -431,72 +323,48 @@ export const deleteStockMovement = async (req: AuthRequest, res: Response) => {
|
|||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const userName = req.appUser?.name || req.appUser?.email || 'Unknown User';
|
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';
|
const isAdmin = req.appUser?.role === 'admin' || req.appUser?.organizationRole === 'admin';
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) return res.status(403).json({ error: 'No admin permissions.' });
|
||||||
return res.status(403).json({ error: 'Apenas administradores podem excluir movimentações.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const movement = await StockMovement.findOne({ _id: id, organizationId });
|
const movement = await StockMovement.findOne({ id, organizationId });
|
||||||
if (!movement) return res.status(404).json({ error: 'Movimentação não encontrada.' });
|
if (!movement) return res.status(404).json({ error: 'Not found.' });
|
||||||
|
|
||||||
const item = await StockItem.findOne({ _id: movement.stockItemId, organizationId });
|
const item = await StockItem.findOne({ id: movement.stockItemId, organizationId });
|
||||||
if (!item) return res.status(404).json({ error: 'Item de estoque associado não encontrado.' });
|
if (!item) return res.status(404).json({ error: 'Item associate not found.' });
|
||||||
|
|
||||||
// Reverse the effect
|
const newStockLevel = Number(item.quantity) - Number(movement.quantity);
|
||||||
// If we delete an Entry (+10), we MUST subtract 10 from Item.
|
if (newStockLevel < 0) return res.status(400).json({ error: 'Negative stock results.' });
|
||||||
// If we delete a Consumption (-10), we MUST add 10 (subtract -10) to Item.
|
|
||||||
// So: Item.quantity -= movement.quantity
|
|
||||||
|
|
||||||
const reverseQty = Number(movement.quantity);
|
await StockItem.findOneAndUpdate({ id: item.id }, { quantity: newStockLevel });
|
||||||
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<string, string> = { ENTRY: 'ENTRADA', CONSUMPTION: 'CONSUMO', ADJUSTMENT: 'AJUSTE' };
|
|
||||||
const typeLabel = typeMap[movement.type] || movement.type;
|
|
||||||
|
|
||||||
await StockAuditLog.create({
|
await StockAuditLog.create({
|
||||||
organizationId,
|
organizationId,
|
||||||
stockItemId: item._id,
|
stockItemId: item.id,
|
||||||
movementId: movement._id,
|
movementId: movement.id,
|
||||||
movementNumber: movement.movementNumber,
|
movementNumber: movement.movementNumber,
|
||||||
userId,
|
userId,
|
||||||
userName,
|
userName,
|
||||||
action: 'DELETE',
|
action: 'DELETE',
|
||||||
details: `Exclusão de Movimentação (#${movement.movementNumber || '?'} ${typeLabel}): Qtd ${movement.quantity}`,
|
details: `Exclusão: Qtd ${movement.quantity}`,
|
||||||
oldValues: movement.toObject()
|
oldValues: movement
|
||||||
});
|
});
|
||||||
|
|
||||||
await StockMovement.deleteOne({ _id: id });
|
await StockMovement.findOneAndDelete({ id });
|
||||||
|
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
|
||||||
console.error('Error deleting movement:', error);
|
|
||||||
res.status(500).json({ error: message });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStockAuditLogs = async (req: AuthRequest, res: Response) => {
|
export const getStockAuditLogs = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params; // StockItem ID
|
const { id } = req.params;
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
|
const logs = await StockAuditLog.find({ stockItemId: id, organizationId });
|
||||||
const logs = await StockAuditLog.find({ stockItemId: id, organizationId })
|
|
||||||
.sort({ timestamp: -1 });
|
|
||||||
|
|
||||||
res.json(logs);
|
res.json(logs);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
|
||||||
res.status(500).json({ error: message });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,44 +1,136 @@
|
|||||||
import { supabase, findOneGpi, queryGpi, insertGpi, updateGpi, deleteGpi } from '../config/supabase.js';
|
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) {
|
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 {
|
return {
|
||||||
find: async (query: any = {}) => {
|
find: function(query: any = {}) {
|
||||||
const { data, error } = await queryGpi(tableName, { filter: query });
|
const dbQuery = mapToDb(query);
|
||||||
if (error) throw error;
|
const promise = (async () => {
|
||||||
return data || [];
|
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) => {
|
findOne: async (query: any) => {
|
||||||
return await findOneGpi(tableName, query);
|
const data = await findOneGpi(tableName, mapToDb(query));
|
||||||
|
return mapFromDb(data);
|
||||||
},
|
},
|
||||||
findById: async (id: string) => {
|
findById: async (id: string) => {
|
||||||
return await findOneGpi(tableName, { id });
|
const data = await findOneGpi(tableName, { id });
|
||||||
|
return mapFromDb(data);
|
||||||
},
|
},
|
||||||
create: async (data: any) => {
|
create: async (data: any) => {
|
||||||
const result = await insertGpi(tableName, data);
|
const dbData = mapToDb(data);
|
||||||
return result.data?.[0] || result.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) => {
|
findOneAndUpdate: async (query: any, update: any) => {
|
||||||
const existing = await findOneGpi(tableName, query);
|
const existing = await findOneGpi(tableName, mapToDb(query));
|
||||||
if (!existing) return null;
|
if (!existing) return null;
|
||||||
const result = await updateGpi(tableName, existing.id, update);
|
const result = await updateGpi(tableName, existing.id, mapToDb(update));
|
||||||
return result.data?.[0];
|
return mapFromDb(result.data?.[0]);
|
||||||
},
|
},
|
||||||
findByIdAndUpdate: async (id: string, update: any) => {
|
findByIdAndUpdate: async (id: string, update: any) => {
|
||||||
const result = await updateGpi(tableName, id, update);
|
const result = await updateGpi(tableName, id, mapToDb(update));
|
||||||
return result.data?.[0];
|
return mapFromDb(result.data?.[0]);
|
||||||
},
|
|
||||||
findOneAndDelete: async (query: any) => {
|
|
||||||
const existing = await findOneGpi(tableName, query);
|
|
||||||
if (!existing) return null;
|
|
||||||
await deleteGpi(tableName, existing.id);
|
|
||||||
return existing;
|
|
||||||
},
|
},
|
||||||
findByIdAndDelete: async (id: string) => {
|
findByIdAndDelete: async (id: string) => {
|
||||||
await deleteGpi(tableName, id);
|
await deleteGpi(tableName, id);
|
||||||
return { 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 };
|
export { queryGpi, findOneGpi, insertGpi, updateGpi, deleteGpi };
|
||||||
|
|
||||||
console.log('✅ Mongoose Compatibility Layer loaded');
|
console.log('✅ Mongoose Compatibility Layer load complete (Extended Mode)');
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
import Project from '../models/Project.js';
|
import {
|
||||||
import Inspection from '../models/Inspection.js';
|
Project, Inspection, ApplicationRecord, TechnicalDataSheet, PaintingScheme,
|
||||||
import ApplicationRecord from '../models/ApplicationRecord.js';
|
Part, Instrument, YieldStudy, GeometryType, StockItem, StockMovement
|
||||||
import TechnicalDataSheet from '../models/TechnicalDataSheet.js';
|
} from '../lib/compat.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';
|
|
||||||
|
|
||||||
interface BackupData {
|
interface BackupData {
|
||||||
version: string;
|
version: string;
|
||||||
@@ -48,17 +42,17 @@ export const backupService = {
|
|||||||
stockItems,
|
stockItems,
|
||||||
stockMovements
|
stockMovements
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
Project.find({ organizationId }).lean(),
|
Project.find({ organizationId }),
|
||||||
Inspection.find({ organizationId }).lean(),
|
Inspection.find({ organizationId }),
|
||||||
ApplicationRecord.find({ organizationId }).lean(),
|
ApplicationRecord.find({ organizationId }),
|
||||||
TechnicalDataSheet.find({ organizationId }).lean(),
|
TechnicalDataSheet.find({ organizationId }),
|
||||||
PaintingScheme.find({ organizationId }).lean(),
|
PaintingScheme.find({ organizationId }),
|
||||||
Part.find({ organizationId }).lean(),
|
Part.find({ organizationId }),
|
||||||
Instrument.find({ organizationId }).lean(),
|
Instrument.find({ organizationId }),
|
||||||
YieldStudy.find({ organizationId }).lean(),
|
YieldStudy.find({ organizationId }),
|
||||||
GeometryType.find({ organizationId }).lean(),
|
GeometryType.find({ organizationId }),
|
||||||
StockItem.find({ organizationId }).lean(),
|
StockItem.find({ organizationId }),
|
||||||
StockMovement.find({ organizationId }).lean()
|
StockMovement.find({ organizationId })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const backup: BackupData = {
|
const backup: BackupData = {
|
||||||
|
|||||||
@@ -1,25 +1,28 @@
|
|||||||
import YieldStudy from '../models/YieldStudy.js';
|
import { YieldStudy, TechnicalDataSheet } from '../lib/compat.js';
|
||||||
|
|
||||||
export const getAllStudies = async (organizationId?: string) => {
|
export const getAllStudies = async (organizationId?: string) => {
|
||||||
const query = organizationId ? { organizationId } : {};
|
const query = organizationId ? { organizationId } : {};
|
||||||
const studies = await YieldStudy.find(query).populate('dataSheetId').sort({ createdAt: -1 }).lean();
|
// Compatibility layer handles .find() and returns plain objects with id
|
||||||
return studies.map(s => ({ ...s, id: s._id.toString() }));
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export const createStudy = async (data: any & { organizationId?: string }) => {
|
export const createStudy = async (data: any & { organizationId?: string }) => {
|
||||||
const newStudy = new YieldStudy({ ...data, organizationId: data.organizationId });
|
const saved = await YieldStudy.create({ ...data, organizationId: data.organizationId });
|
||||||
const saved = await newStudy.save();
|
return saved;
|
||||||
return { ...saved.toObject(), id: saved._id.toString() };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
// 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) => {
|
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);
|
const existing = await YieldStudy.findById(id);
|
||||||
if (!existing) return null;
|
if (!existing) return null;
|
||||||
|
|
||||||
@@ -32,15 +35,11 @@ export const updateStudy = async (id: string, updates: any, organizationId?: str
|
|||||||
updates.organizationId = organizationId;
|
updates.organizationId = organizationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = await YieldStudy.findOneAndUpdate({ _id: id }, updates, { new: true }).lean();
|
const updated = await YieldStudy.findOneAndUpdate({ id }, updates);
|
||||||
if (updated) {
|
return updated;
|
||||||
return { ...updated, id: updated._id.toString() };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteStudy = async (id: string, organizationId?: string) => {
|
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);
|
const existing = await YieldStudy.findById(id);
|
||||||
if (!existing) return false;
|
if (!existing) return false;
|
||||||
|
|
||||||
@@ -52,5 +51,3 @@ export const deleteStudy = async (id: string, organizationId?: string) => {
|
|||||||
await YieldStudy.findByIdAndDelete(id);
|
await YieldStudy.findByIdAndDelete(id);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user