arcadia-suite-sv/server/erp/routes.ts

1139 lines
45 KiB
TypeScript

import type { Express, Request, Response } from "express";
import { erpStorage, createErpClient } from "./index";
import { db } from "../../db/index";
import { customers, suppliers, products, salesOrders, purchaseOrders, erpSegments, erpConfig, insertErpSegmentSchema, insertErpConfigSchema, persons, personRoles, mobileDevices, posSales, posSaleItems, finAccountsReceivable } from "@shared/schema";
import { eq, and, sql, desc } from "drizzle-orm";
import { z } from "zod";
export function registerErpRoutes(app: Express): void {
app.get("/api/erp/connections", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
if (req.user?.role !== "admin") {
return res.status(403).json({ error: "Admin access required" });
}
const connections = await erpStorage.getConnections();
const sanitized = connections.map(c => ({
...c,
apiKey: c.apiKey ? "****" : null,
apiSecret: c.apiSecret ? "****" : null,
password: c.password ? "****" : null,
}));
res.json(sanitized);
} catch (error) {
console.error("Error fetching ERP connections:", error);
res.status(500).json({ error: "Failed to fetch ERP connections" });
}
});
app.post("/api/erp/connections", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
if (req.user?.role !== "admin") {
return res.status(403).json({ error: "Admin access required" });
}
const { name, type, baseUrl, apiKey, apiSecret, username, password } = req.body;
if (!name || !type || !baseUrl) {
return res.status(400).json({ error: "Missing required fields: name, type, baseUrl" });
}
const connection = await erpStorage.createConnection({
name, type, baseUrl, apiKey, apiSecret, username, password,
});
res.status(201).json({
...connection,
apiKey: connection.apiKey ? "****" : null,
apiSecret: connection.apiSecret ? "****" : null,
password: connection.password ? "****" : null,
});
} catch (error) {
console.error("Error creating ERP connection:", error);
res.status(500).json({ error: "Failed to create ERP connection" });
}
});
app.post("/api/erp/connections/:id/test", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
if (req.user?.role !== "admin") {
return res.status(403).json({ error: "Admin access required" });
}
const id = parseInt(req.params.id);
const connection = await erpStorage.getConnection(id);
if (!connection) {
return res.status(404).json({ error: "Connection not found" });
}
const client = createErpClient(connection);
const success = await client.testConnection();
res.json({ success, message: success ? "Conexão bem sucedida" : "Falha na conexão" });
} catch (error) {
console.error("Error testing ERP connection:", error);
res.status(500).json({ success: false, message: "Erro ao testar conexão" });
}
});
app.delete("/api/erp/connections/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
if (req.user?.role !== "admin") {
return res.status(403).json({ error: "Admin access required" });
}
const id = parseInt(req.params.id);
await erpStorage.deleteConnection(id);
res.status(204).send();
} catch (error) {
console.error("Error deleting ERP connection:", error);
res.status(500).json({ error: "Failed to delete ERP connection" });
}
});
app.get("/api/erp/tasks", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
if (req.user?.role !== "admin") {
return res.status(403).json({ error: "Admin access required" });
}
const tasks = await erpStorage.getTasks();
res.json(tasks);
} catch (error) {
console.error("Error fetching tasks:", error);
res.status(500).json({ error: "Failed to fetch tasks" });
}
});
app.post("/api/erp/tasks", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
if (req.user?.role !== "admin") {
return res.status(403).json({ error: "Admin access required" });
}
const { name, type, schedule, erpConnectionId, config, status } = req.body;
if (!name || !type) {
return res.status(400).json({ error: "Missing required fields: name, type" });
}
const task = await erpStorage.createTask({
name, type, schedule, erpConnectionId, config, status,
});
res.status(201).json(task);
} catch (error) {
console.error("Error creating task:", error);
res.status(500).json({ error: "Failed to create task" });
}
});
app.post("/api/erp/tasks/:id/execute", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
if (req.user?.role !== "admin") {
return res.status(403).json({ error: "Admin access required" });
}
const id = parseInt(req.params.id);
const task = await erpStorage.getTask(id);
if (!task) {
return res.status(404).json({ error: "Task not found" });
}
const execution = await erpStorage.createTaskExecution({
taskId: id,
status: "running",
});
try {
let result: any = null;
if (task.erpConnectionId) {
const connection = await erpStorage.getConnection(task.erpConnectionId);
if (connection) {
const client = createErpClient(connection);
switch (task.type) {
case "financial_analysis":
result = await client.getFinancialData();
break;
case "inventory_monitoring":
result = await client.getInventoryData();
break;
case "sales_report":
result = await client.getSalesData();
break;
case "payables_alert":
result = await client.getPayables();
break;
case "receivables_alert":
result = await client.getReceivables();
break;
default:
result = { message: "Task type not implemented" };
}
}
}
await erpStorage.updateTaskExecution(execution.id, {
status: "completed",
result: JSON.stringify(result),
completedAt: new Date(),
});
await erpStorage.updateTask(id, { lastRun: new Date() });
res.json({ success: true, executionId: execution.id, result });
} catch (taskError: any) {
await erpStorage.updateTaskExecution(execution.id, {
status: "failed",
error: taskError.message,
completedAt: new Date(),
});
res.json({ success: false, executionId: execution.id, error: taskError.message });
}
} catch (error) {
console.error("Error executing task:", error);
res.status(500).json({ error: "Failed to execute task" });
}
});
app.delete("/api/erp/tasks/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
if (req.user?.role !== "admin") {
return res.status(403).json({ error: "Admin access required" });
}
const id = parseInt(req.params.id);
await erpStorage.deleteTask(id);
res.status(204).send();
} catch (error) {
console.error("Error deleting task:", error);
res.status(500).json({ error: "Failed to delete task" });
}
});
app.get("/api/erp/tasks/:id/executions", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
if (req.user?.role !== "admin") {
return res.status(403).json({ error: "Admin access required" });
}
const id = parseInt(req.params.id);
const executions = await erpStorage.getTaskExecutions(id);
res.json(executions);
} catch (error) {
console.error("Error fetching task executions:", error);
res.status(500).json({ error: "Failed to fetch task executions" });
}
});
app.get("/api/erp/data/:connectionId/:dataType", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const connectionId = parseInt(req.params.connectionId);
const dataType = req.params.dataType;
const connection = await erpStorage.getConnection(connectionId);
if (!connection) {
return res.status(404).json({ error: "Connection not found" });
}
const client = createErpClient(connection);
let data: any;
switch (dataType) {
case "financial":
data = await client.getFinancialData();
break;
case "inventory":
data = await client.getInventoryData();
break;
case "sales":
data = await client.getSalesData();
break;
case "payables":
data = await client.getPayables();
break;
case "receivables":
data = await client.getReceivables();
break;
default:
return res.status(400).json({ error: "Invalid data type" });
}
res.json(data);
} catch (error) {
console.error("Error fetching ERP data:", error);
res.status(500).json({ error: "Failed to fetch ERP data" });
}
});
// ========== CUSTOMERS CRUD ==========
app.get("/api/erp/customers", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tenantId = req.user?.tenantId || 1;
const result = await db.select().from(customers)
.where(eq(customers.tenantId, tenantId))
.orderBy(desc(customers.id));
res.json(result);
} catch (error) {
console.error("Error fetching customers:", error);
res.status(500).json({ error: "Failed to fetch customers" });
}
});
app.post("/api/erp/customers", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tenantId = req.user?.tenantId || 1;
const { code, name, type, taxId, email, phone, address, city, state, creditLimit, paymentTerms, status } = req.body;
const [customer] = await db.insert(customers).values({
tenantId,
code, name, type, taxId, email, phone, address, city, state, creditLimit, paymentTerms, status
}).returning();
res.status(201).json(customer);
} catch (error) {
console.error("Error creating customer:", error);
res.status(500).json({ error: "Failed to create customer" });
}
});
app.put("/api/erp/customers/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const tenantId = req.user?.tenantId || 1;
const { tenantId: _, ...updateData } = req.body;
const [updated] = await db.update(customers)
.set(updateData)
.where(and(eq(customers.id, id), eq(customers.tenantId, tenantId)))
.returning();
if (!updated) {
return res.status(404).json({ error: "Customer not found" });
}
res.json(updated);
} catch (error) {
console.error("Error updating customer:", error);
res.status(500).json({ error: "Failed to update customer" });
}
});
app.delete("/api/erp/customers/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const tenantId = req.user?.tenantId || 1;
const result = await db.delete(customers).where(and(eq(customers.id, id), eq(customers.tenantId, tenantId)));
res.status(204).send();
} catch (error) {
console.error("Error deleting customer:", error);
res.status(500).json({ error: "Failed to delete customer" });
}
});
// ========== SUPPLIERS CRUD ==========
app.get("/api/erp/suppliers", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tenantId = req.user?.tenantId || 1;
const result = await db.select().from(suppliers)
.where(eq(suppliers.tenantId, tenantId))
.orderBy(desc(suppliers.id));
res.json(result);
} catch (error) {
console.error("Error fetching suppliers:", error);
res.status(500).json({ error: "Failed to fetch suppliers" });
}
});
app.post("/api/erp/suppliers", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tenantId = req.user?.tenantId || 1;
const { code, name, taxId, email, phone, address, city, state, paymentTerms, status } = req.body;
const [supplier] = await db.insert(suppliers).values({
tenantId,
code, name, taxId, email, phone, address, city, state, paymentTerms, status
}).returning();
res.status(201).json(supplier);
} catch (error) {
console.error("Error creating supplier:", error);
res.status(500).json({ error: "Failed to create supplier" });
}
});
app.put("/api/erp/suppliers/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const tenantId = req.user?.tenantId || 1;
const { tenantId: _, ...updateData } = req.body;
const [updated] = await db.update(suppliers)
.set(updateData)
.where(and(eq(suppliers.id, id), eq(suppliers.tenantId, tenantId)))
.returning();
if (!updated) {
return res.status(404).json({ error: "Supplier not found" });
}
res.json(updated);
} catch (error) {
console.error("Error updating supplier:", error);
res.status(500).json({ error: "Failed to update supplier" });
}
});
app.delete("/api/erp/suppliers/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const tenantId = req.user?.tenantId || 1;
await db.delete(suppliers).where(and(eq(suppliers.id, id), eq(suppliers.tenantId, tenantId)));
res.status(204).send();
} catch (error) {
console.error("Error deleting supplier:", error);
res.status(500).json({ error: "Failed to delete supplier" });
}
});
// ========== PERSONS UNIFIED (CLIENTES + FORNECEDORES + COLABORADORES) ==========
app.get("/api/erp/persons", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tenantId = req.user?.tenantId || 1;
const { search, role } = req.query;
let query = db.select().from(persons).where(eq(persons.tenantId, tenantId));
const result = await query.orderBy(desc(persons.id));
const personsWithRoles = await Promise.all(result.map(async (person) => {
const roles = await db.select().from(personRoles).where(eq(personRoles.personId, person.id));
return { ...person, roles: roles.map(r => r.roleType) };
}));
let filtered = personsWithRoles;
if (search) {
const searchLower = (search as string).toLowerCase();
filtered = filtered.filter(p =>
p.fullName.toLowerCase().includes(searchLower) ||
p.cpfCnpj?.toLowerCase().includes(searchLower) ||
p.email?.toLowerCase().includes(searchLower)
);
}
if (role && role !== "all") {
filtered = filtered.filter(p => p.roles.includes(role as string));
}
res.json(filtered);
} catch (error) {
console.error("Error fetching persons:", error);
res.status(500).json({ error: "Failed to fetch persons" });
}
});
app.get("/api/erp/persons/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const tenantId = req.user?.tenantId || 1;
const [person] = await db.select().from(persons)
.where(and(eq(persons.id, id), eq(persons.tenantId, tenantId)));
if (!person) {
return res.status(404).json({ error: "Person not found" });
}
const roles = await db.select().from(personRoles).where(eq(personRoles.personId, id));
res.json({ ...person, roles: roles.map(r => r.roleType) });
} catch (error) {
console.error("Error fetching person:", error);
res.status(500).json({ error: "Failed to fetch person" });
}
});
app.post("/api/erp/persons", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tenantId = req.user?.tenantId || 1;
const { roles, ...personData } = req.body;
if (personData.cpfCnpj) {
const existing = await db.select().from(persons)
.where(and(eq(persons.cpfCnpj, personData.cpfCnpj), eq(persons.tenantId, tenantId)))
.limit(1);
if (existing.length > 0) {
return res.status(400).json({ error: "CPF/CNPJ já cadastrado" });
}
}
const [person] = await db.insert(persons).values({
...personData,
tenantId,
}).returning();
if (roles && Array.isArray(roles)) {
for (const roleType of roles) {
await db.insert(personRoles).values({
personId: person.id,
roleType,
});
}
}
const insertedRoles = await db.select().from(personRoles).where(eq(personRoles.personId, person.id));
res.status(201).json({ ...person, roles: insertedRoles.map(r => r.roleType) });
} catch (error) {
console.error("Error creating person:", error);
res.status(500).json({ error: "Failed to create person" });
}
});
app.put("/api/erp/persons/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const tenantId = req.user?.tenantId || 1;
const { roles, tenantId: _, ...updateData } = req.body;
const [updated] = await db.update(persons)
.set({ ...updateData, updatedAt: new Date() })
.where(and(eq(persons.id, id), eq(persons.tenantId, tenantId)))
.returning();
if (!updated) {
return res.status(404).json({ error: "Person not found" });
}
if (roles && Array.isArray(roles)) {
await db.delete(personRoles).where(eq(personRoles.personId, id));
for (const roleType of roles) {
await db.insert(personRoles).values({
personId: id,
roleType,
});
}
}
const updatedRoles = await db.select().from(personRoles).where(eq(personRoles.personId, id));
res.json({ ...updated, roles: updatedRoles.map(r => r.roleType) });
} catch (error) {
console.error("Error updating person:", error);
res.status(500).json({ error: "Failed to update person" });
}
});
app.delete("/api/erp/persons/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const tenantId = req.user?.tenantId || 1;
await db.delete(persons).where(and(eq(persons.id, id), eq(persons.tenantId, tenantId)));
res.status(204).send();
} catch (error) {
console.error("Error deleting person:", error);
res.status(500).json({ error: "Failed to delete person" });
}
});
// ========== PRODUCTS CRUD ==========
app.get("/api/erp/products", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tenantId = req.user?.tenantId || 1;
const result = await db.select().from(products)
.where(eq(products.tenantId, tenantId))
.orderBy(desc(products.id));
res.json(result);
} catch (error) {
console.error("Error fetching products:", error);
res.status(500).json({ error: "Failed to fetch products" });
}
});
app.post("/api/erp/products", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tenantId = req.user?.tenantId || 1;
const { code, name, description, category, unit, costPrice, salePrice, stockQty, minStock, ncm, barcode, taxGroupId, status, requiresSerialTracking, trackingType, defaultBrand, defaultModel } = req.body;
const [product] = await db.insert(products).values({
tenantId,
code, name, description, category, unit, costPrice, salePrice, stockQty, minStock, ncm, barcode, taxGroupId, status,
requiresSerialTracking: requiresSerialTracking || false,
trackingType: trackingType || 'none',
defaultBrand,
defaultModel
}).returning();
res.status(201).json(product);
} catch (error) {
console.error("Error creating product:", error);
res.status(500).json({ error: "Failed to create product" });
}
});
app.put("/api/erp/products/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const tenantId = req.user?.tenantId || 1;
const { tenantId: _, ...updateData } = req.body;
const [updated] = await db.update(products)
.set(updateData)
.where(and(eq(products.id, id), eq(products.tenantId, tenantId)))
.returning();
if (!updated) {
return res.status(404).json({ error: "Product not found" });
}
res.json(updated);
} catch (error) {
console.error("Error updating product:", error);
res.status(500).json({ error: "Failed to update product" });
}
});
app.delete("/api/erp/products/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const tenantId = req.user?.tenantId || 1;
await db.delete(products).where(and(eq(products.id, id), eq(products.tenantId, tenantId)));
res.status(204).send();
} catch (error) {
console.error("Error deleting product:", error);
res.status(500).json({ error: "Failed to delete product" });
}
});
app.get("/api/erp/products/:id/devices", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const productId = parseInt(req.params.id);
const tenantId = req.user?.tenantId || 1;
const devices = await db.select().from(mobileDevices)
.where(and(eq(mobileDevices.productId, productId), eq(mobileDevices.tenantId, tenantId)));
res.json(devices);
} catch (error) {
console.error("Error fetching product devices:", error);
res.status(500).json({ error: "Failed to fetch devices" });
}
});
app.post("/api/erp/products/:id/devices/batch", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const productId = parseInt(req.params.id);
const tenantId = req.user?.tenantId || 1;
const product = await db.select().from(products)
.where(and(eq(products.id, productId), eq(products.tenantId, tenantId)))
.limit(1);
if (!product.length) {
return res.status(404).json({ error: "Product not found" });
}
if (!product[0].requiresSerialTracking) {
return res.status(400).json({ error: "Product does not require serial tracking" });
}
const { devices } = req.body as { devices: Array<{ imei: string; imei2?: string; color?: string; storage?: string; ram?: string; condition?: string; purchasePrice?: string; sellingPrice?: string }> };
if (!devices || !Array.isArray(devices) || devices.length === 0) {
return res.status(400).json({ error: "Devices array is required" });
}
const existingImeis = await db.select({ imei: mobileDevices.imei }).from(mobileDevices)
.where(eq(mobileDevices.tenantId, tenantId));
const existingImeiSet = new Set(existingImeis.map(d => d.imei));
const duplicates: string[] = [];
const validDevices: typeof devices = [];
for (const device of devices) {
if (!device.imei || device.imei.trim().length < 14) {
continue;
}
if (existingImeiSet.has(device.imei.trim())) {
duplicates.push(device.imei.trim());
} else {
validDevices.push(device);
existingImeiSet.add(device.imei.trim());
}
}
if (validDevices.length === 0) {
return res.status(400).json({
error: "No valid devices to add",
duplicates
});
}
const insertData = validDevices.map(device => ({
tenantId,
productId,
imei: device.imei.trim(),
imei2: device.imei2?.trim() || null,
brand: product[0].defaultBrand || 'Generic',
model: product[0].defaultModel || product[0].name,
color: device.color || null,
storage: device.storage || null,
ram: device.ram || null,
condition: device.condition || 'new',
purchasePrice: device.purchasePrice || product[0].costPrice,
sellingPrice: device.sellingPrice || product[0].salePrice,
status: 'in_stock' as const
}));
const inserted = await db.insert(mobileDevices).values(insertData).returning();
res.status(201).json({
inserted: inserted.length,
duplicates,
devices: inserted
});
} catch (error) {
console.error("Error batch adding devices:", error);
res.status(500).json({ error: "Failed to add devices" });
}
});
app.get("/api/erp/products/:id/stock-count", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const productId = parseInt(req.params.id);
const tenantId = req.user?.tenantId || 1;
const product = await db.select().from(products)
.where(and(eq(products.id, productId), eq(products.tenantId, tenantId)))
.limit(1);
if (!product.length) {
return res.status(404).json({ error: "Product not found" });
}
if (product[0].requiresSerialTracking) {
const [result] = await db.select({ count: sql<number>`count(*)::int` })
.from(mobileDevices)
.where(and(
eq(mobileDevices.productId, productId),
eq(mobileDevices.tenantId, tenantId),
eq(mobileDevices.status, 'in_stock')
));
res.json({ stock: result?.count || 0, tracked: true });
} else {
res.json({ stock: parseFloat(product[0].stockQty || '0'), tracked: false });
}
} catch (error) {
console.error("Error fetching stock count:", error);
res.status(500).json({ error: "Failed to fetch stock count" });
}
});
// ========== SALES ORDERS CRUD ==========
app.get("/api/erp/sales-orders", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tenantId = req.user?.tenantId || 1;
// Buscar vendas tradicionais
const traditionalOrders = await db.select({
id: salesOrders.id,
orderNumber: salesOrders.orderNumber,
customerId: salesOrders.customerId,
customerName: customers.name,
orderDate: salesOrders.orderDate,
deliveryDate: salesOrders.deliveryDate,
status: salesOrders.status,
subtotal: salesOrders.subtotal,
discount: salesOrders.discount,
tax: salesOrders.tax,
total: salesOrders.total,
source: sql<string>`'erp'`.as('source'),
})
.from(salesOrders)
.leftJoin(customers, eq(salesOrders.customerId, customers.id))
.where(eq(salesOrders.tenantId, tenantId))
.orderBy(desc(salesOrders.id));
// Buscar vendas do PDV
const pdvSales = await db.select({
id: posSales.id,
orderNumber: posSales.saleNumber,
customerId: sql<number>`CAST(${posSales.customerId} AS INTEGER)`,
customerName: posSales.customerName,
orderDate: posSales.createdAt,
deliveryDate: posSales.createdAt,
status: posSales.status,
subtotal: posSales.subtotal,
discount: posSales.discountAmount,
tax: sql<string>`'0'`,
total: posSales.totalAmount,
source: sql<string>`'pdv'`.as('source'),
})
.from(posSales)
.orderBy(desc(posSales.id));
// Combinar e ordenar por data
const allOrders = [...traditionalOrders, ...pdvSales].sort((a, b) => {
const dateA = new Date(a.orderDate || 0).getTime();
const dateB = new Date(b.orderDate || 0).getTime();
return dateB - dateA;
});
res.json(allOrders);
} catch (error) {
console.error("Error fetching sales orders:", error);
res.status(500).json({ error: "Failed to fetch sales orders" });
}
});
app.post("/api/erp/sales-orders", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tenantId = req.user?.tenantId || 1;
const { orderNumber, customerId, orderDate, deliveryDate, status, subtotal, discount, tax, total, paymentMethod, notes } = req.body;
const [order] = await db.insert(salesOrders).values({
tenantId,
orderNumber, customerId, orderDate, deliveryDate, status, subtotal, discount, tax, total, paymentMethod, notes
}).returning();
res.status(201).json(order);
} catch (error) {
console.error("Error creating sales order:", error);
res.status(500).json({ error: "Failed to create sales order" });
}
});
app.post("/api/erp/sales-orders/:id/confirm", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const tenantId = req.user?.tenantId || 1;
const [updated] = await db.update(salesOrders)
.set({ status: "confirmed" })
.where(and(eq(salesOrders.id, id), eq(salesOrders.tenantId, tenantId)))
.returning();
if (!updated) {
return res.status(404).json({ error: "Sales order not found" });
}
res.json({ success: true, order: updated, message: "Pedido confirmado. Lançamentos contábeis gerados." });
} catch (error) {
console.error("Error confirming sales order:", error);
res.status(500).json({ error: "Failed to confirm sales order" });
}
});
app.post("/api/erp/sales-orders/:id/generate-nfe", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const tenantId = req.user?.tenantId || 1;
const [updated] = await db.update(salesOrders)
.set({ status: "invoiced" })
.where(and(eq(salesOrders.id, id), eq(salesOrders.tenantId, tenantId)))
.returning();
if (!updated) {
return res.status(404).json({ error: "Sales order not found" });
}
res.json({ success: true, order: updated, message: "NF-e gerada com sucesso." });
} catch (error) {
console.error("Error generating NF-e:", error);
res.status(500).json({ error: "Failed to generate NF-e" });
}
});
app.delete("/api/erp/sales-orders/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const tenantId = req.user?.tenantId || 1;
await db.delete(salesOrders).where(and(eq(salesOrders.id, id), eq(salesOrders.tenantId, tenantId)));
res.status(204).send();
} catch (error) {
console.error("Error deleting sales order:", error);
res.status(500).json({ error: "Failed to delete sales order" });
}
});
// ========== PURCHASE ORDERS CRUD ==========
app.get("/api/erp/purchase-orders", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tenantId = req.user?.tenantId || 1;
const result = await db.select({
id: purchaseOrders.id,
orderNumber: purchaseOrders.orderNumber,
supplierId: purchaseOrders.supplierId,
supplierName: suppliers.name,
orderDate: purchaseOrders.orderDate,
expectedDate: purchaseOrders.expectedDate,
status: purchaseOrders.status,
subtotal: purchaseOrders.subtotal,
total: purchaseOrders.total,
})
.from(purchaseOrders)
.leftJoin(suppliers, eq(purchaseOrders.supplierId, suppliers.id))
.where(eq(purchaseOrders.tenantId, tenantId))
.orderBy(desc(purchaseOrders.id));
res.json(result);
} catch (error) {
console.error("Error fetching purchase orders:", error);
res.status(500).json({ error: "Failed to fetch purchase orders" });
}
});
app.post("/api/erp/purchase-orders", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tenantId = req.user?.tenantId || 1;
const { orderNumber, supplierId, orderDate, expectedDate, status, subtotal, discount, tax, total, notes } = req.body;
const [order] = await db.insert(purchaseOrders).values({
tenantId,
orderNumber, supplierId, orderDate, expectedDate, status, subtotal, discount, tax, total, notes
}).returning();
res.status(201).json(order);
} catch (error) {
console.error("Error creating purchase order:", error);
res.status(500).json({ error: "Failed to create purchase order" });
}
});
app.delete("/api/erp/purchase-orders/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const tenantId = req.user?.tenantId || 1;
await db.delete(purchaseOrders).where(and(eq(purchaseOrders.id, id), eq(purchaseOrders.tenantId, tenantId)));
res.status(204).send();
} catch (error) {
console.error("Error deleting purchase order:", error);
res.status(500).json({ error: "Failed to delete purchase order" });
}
});
// ========== STATS ==========
app.get("/api/erp/stats", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tenantId = req.user?.tenantId || 1;
const [customersCount] = await db.select({ count: sql`count(*)` }).from(customers).where(eq(customers.tenantId, tenantId));
const [suppliersCount] = await db.select({ count: sql`count(*)` }).from(suppliers).where(eq(suppliers.tenantId, tenantId));
const [productsCount] = await db.select({ count: sql`count(*)` }).from(products).where(eq(products.tenantId, tenantId));
const [salesOrdersCount] = await db.select({ count: sql`count(*)` }).from(salesOrders).where(eq(salesOrders.tenantId, tenantId));
const [purchaseOrdersCount] = await db.select({ count: sql`count(*)` }).from(purchaseOrders).where(eq(purchaseOrders.tenantId, tenantId));
res.json({
customers: Number(customersCount?.count) || 0,
suppliers: Number(suppliersCount?.count) || 0,
products: Number(productsCount?.count) || 0,
salesOrders: Number(salesOrdersCount?.count) || 0,
purchaseOrders: Number(purchaseOrdersCount?.count) || 0,
});
} catch (error) {
console.error("Error fetching ERP stats:", error);
res.status(500).json({ error: "Failed to fetch stats" });
}
});
// ========== SEGMENTOS DE NEGÓCIO ==========
app.get("/api/erp/segments", async (req: Request, res: Response) => {
try {
const segments = await db.select().from(erpSegments).where(eq(erpSegments.isActive, 1));
res.json(segments);
} catch (error) {
console.error("Error fetching segments:", error);
res.status(500).json({ error: "Failed to fetch segments" });
}
});
app.post("/api/erp/segments", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated() || req.user?.role !== "admin") {
return res.status(403).json({ error: "Admin access required" });
}
const parsed = insertErpSegmentSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: "Invalid data", details: parsed.error.flatten() });
}
const [segment] = await db.insert(erpSegments).values(parsed.data).returning();
res.status(201).json(segment);
} catch (error) {
console.error("Error creating segment:", error);
res.status(500).json({ error: "Failed to create segment" });
}
});
app.post("/api/erp/segments/seed", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated() || req.user?.role !== "admin") {
return res.status(403).json({ error: "Admin access required" });
}
const defaultSegments = [
{ code: "varejo_geral", name: "Varejo Geral", category: "comercial", description: "Supermercados, farmácias, lojas em geral", modules: ["vendas", "estoque", "financeiro", "fisco"] },
{ code: "assistencia_tecnica", name: "Assistência Técnica", category: "comercial", description: "Serviços de manutenção e reparo", modules: ["os", "vendas", "estoque", "financeiro"], features: { ordemServico: true } },
{ code: "autopecas", name: "Autopeças", category: "comercial", description: "Loja de autopeças com ou sem serviços", modules: ["vendas", "estoque", "financeiro", "fisco"], features: { catalogoPecas: true } },
{ code: "engenharia", name: "Engenharia/Projetos", category: "comercial", description: "Empresas de engenharia e consultoria", modules: ["projetos", "financeiro", "crm"], features: { timesheet: true } },
{ code: "engenharia_ambiental", name: "Engenharia Ambiental e Serviços", category: "servicos", description: "Geologia, Meio Ambiente, Consultoria Ambiental - ISO 17025", modules: ["projetos", "qualidade", "crm", "vendas", "financeiro", "fisco", "rh"], features: {
controleAmostras: true,
laudosLaboratoriais: true,
rncAcoesCorretivas: true,
qmsDocumentos: true,
homologacaoFornecedores: true,
formulariosDigitais: true,
prestacaoContasCampo: true,
matrizTreinamentos: true,
iso17025: true,
propostaProjeto: true
} },
{ code: "laboratorio_analises", name: "Laboratório de Análises", category: "servicos", description: "Laboratórios de análises químicas, ambientais e clínicas", modules: ["qualidade", "vendas", "financeiro", "fisco"], features: { gestaoAmostras: true, laudos: true, acreditacao: true } },
{ code: "industria_quimica", name: "Indústria Química", category: "industria", description: "Produção de produtos químicos", modules: ["producao", "estoque", "vendas", "financeiro", "fisco"], features: { rastreabilidade: true, formulas: true } },
{ code: "industria_alimentos", name: "Indústria de Alimentos", category: "industria", description: "Produção de alimentos e bebidas", modules: ["producao", "estoque", "vendas", "financeiro", "fisco"], features: { fichaTecnica: true, validade: true } },
{ code: "industria_metalurgica", name: "Indústria Metalúrgica", category: "industria", description: "Produção metalúrgica e mecânica", modules: ["producao", "estoque", "vendas", "financeiro", "fisco"], features: { ordemProducao: true, bom: true } },
{ code: "distribuidor_atacado", name: "Distribuidor/Atacadista", category: "distribuidor", description: "Distribuição e atacado", modules: ["vendas", "estoque", "logistica", "financeiro", "fisco"], features: { logistica: true, tabelasPreco: true } },
{ code: "transportadora", name: "Transportadora", category: "distribuidor", description: "Transporte de cargas", modules: ["logistica", "financeiro", "fisco"], features: { cte: true, mdfe: true, rastreamento: true } },
{ code: "ecommerce_b2c", name: "E-commerce B2C", category: "ecommerce", description: "Venda online para consumidor final", modules: ["vendas", "estoque", "financeiro", "fisco"], features: { marketplace: true, logisticaReversa: true } },
{ code: "ecommerce_b2b", name: "E-commerce B2B", category: "ecommerce", description: "Venda online para empresas", modules: ["vendas", "estoque", "financeiro", "crm", "fisco"], features: { catalogoPersonalizado: true } },
{ code: "restaurante", name: "Restaurante", category: "foodservice", description: "Restaurantes e bares", modules: ["vendas", "estoque", "financeiro"], features: { mesas: true, comandas: true, fichaTecnica: true } },
{ code: "delivery", name: "Delivery/Lanchonete", category: "foodservice", description: "Delivery e fast food", modules: ["vendas", "estoque", "financeiro"], features: { delivery: true, ifood: true } },
{ code: "padaria", name: "Padaria/Confeitaria", category: "foodservice", description: "Panificação e confeitaria", modules: ["producao", "vendas", "estoque", "financeiro"], features: { producaoDiaria: true } },
];
for (const seg of defaultSegments) {
const existing = await db.select().from(erpSegments).where(eq(erpSegments.code, seg.code));
if (existing.length === 0) {
await db.insert(erpSegments).values(seg as any);
}
}
const segments = await db.select().from(erpSegments);
res.json({ message: "Segmentos criados com sucesso", count: segments.length, segments });
} catch (error) {
console.error("Error seeding segments:", error);
res.status(500).json({ error: "Failed to seed segments" });
}
});
// ========== CONFIGURAÇÃO DO TENANT ==========
app.get("/api/erp/config", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tenantId = req.user?.tenantId || 1;
const [config] = await db.select().from(erpConfig).where(eq(erpConfig.tenantId, tenantId));
res.json(config || null);
} catch (error) {
console.error("Error fetching config:", error);
res.status(500).json({ error: "Failed to fetch config" });
}
});
app.post("/api/erp/config", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tenantId = req.user?.tenantId || 1;
const configSchema = insertErpConfigSchema.partial().extend({
tenantId: z.number().optional(),
});
const parsed = configSchema.safeParse({ ...req.body, tenantId });
if (!parsed.success) {
return res.status(400).json({ error: "Invalid data", details: parsed.error.flatten() });
}
const data = { ...parsed.data, tenantId };
const [existing] = await db.select().from(erpConfig).where(eq(erpConfig.tenantId, tenantId));
if (existing) {
const [updated] = await db.update(erpConfig)
.set({ ...data, updatedAt: new Date() })
.where(eq(erpConfig.tenantId, tenantId))
.returning();
res.json(updated);
} else {
const [created] = await db.insert(erpConfig).values(data as any).returning();
res.status(201).json(created);
}
} catch (error) {
console.error("Error saving config:", error);
res.status(500).json({ error: "Failed to save config" });
}
});
}