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`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`'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`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`'0'`, total: posSales.totalAmount, source: sql`'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" }); } }); }