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

5219 lines
191 KiB
TypeScript

import { Router, Request, Response, NextFunction } from "express";
import { db } from "../../db/index";
import {
retailStores, retailWarehouses, mobileDevices, deviceEvaluations,
serviceOrders, serviceOrderItems, posSessions, posSales, posSaleItems,
paymentPlans, paymentPlanInstallments, leaseAgreements, leasePayments,
stockTransfers, stockTransferItems, returnExchanges, returnExchangeItems,
deviceHistory, tradeInChecklistTemplates, tradeInChecklistItems,
tradeInEvaluationResults, tradeInTransferDocuments,
persons, personRoles, imeiHistory, customerCredits, finAccountsReceivable,
retailActivityFeed, retailPaymentMethods, retailSellers, retailCommissionPlans,
retailPriceTables, retailPromotions, retailProductTypes,
retailSellerGoals, retailStoreGoals, retailCommissionClosures, retailCommissionClosureItems,
retailWarehouseStock, retailStockMovements, retailProductSerials,
retailStockTransfers, retailStockTransferItems, retailInventories, retailInventoryItems,
products, purchaseOrders, purchaseOrderItems, suppliers,
tenantEmpresas, tenants, type TenantFeatures,
posCashMovements, serviceWarranties
} from "@shared/schema";
import { eq, desc, and, ilike, sql, or, asc, inArray, gte, lte, isNull, between, count, sum } from "drizzle-orm";
import { retailPlusSyncService } from "./plus-sync";
const router = Router();
const requireModule = (moduleKey: keyof TenantFeatures) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const tenantId = (req.user as any)?.tenantId;
if (!tenantId) {
return res.status(401).json({ error: "Tenant not identified" });
}
const [tenant] = await db.select().from(tenants).where(eq(tenants.id, tenantId));
if (!tenant) {
return res.status(404).json({ error: "Tenant not found" });
}
const features = (tenant?.features as TenantFeatures) || {};
if (!features[moduleKey]) {
return res.status(403).json({
error: `Módulo "${moduleKey}" não está ativo para este tenant`,
moduleKey,
action: "Ative o módulo em Admin → Módulos"
});
}
next();
} catch (error) {
next(error);
}
};
};
const requireAuth = (req: Request, res: Response, next: NextFunction) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
next();
};
router.use(requireAuth);
// Helper function to log activity
async function logActivity(data: {
activityType: string;
entityType?: string;
entityId?: number;
title: string;
description?: string;
metadata?: any;
severity?: string;
createdBy?: string;
createdByName?: string;
tenantId?: number;
storeId?: number;
}) {
try {
await db.insert(retailActivityFeed).values({
activityType: data.activityType,
entityType: data.entityType,
entityId: data.entityId,
title: data.title,
description: data.description,
metadata: data.metadata,
severity: data.severity || "info",
createdBy: data.createdBy,
createdByName: data.createdByName,
tenantId: data.tenantId,
storeId: data.storeId,
});
} catch (error) {
console.error("Error logging activity:", error);
}
}
// ========== ACTIVITY FEED ==========
router.get("/activity-feed", async (req: Request, res: Response) => {
try {
const limit = parseInt(req.query.limit as string) || 50;
const user = (req as any).user;
const tenantId = user?.tenantId;
// Scope by tenant if available
let activities;
if (tenantId) {
activities = await db.select()
.from(retailActivityFeed)
.where(or(
eq(retailActivityFeed.tenantId, tenantId),
sql`${retailActivityFeed.tenantId} IS NULL`
))
.orderBy(desc(retailActivityFeed.createdAt))
.limit(limit);
} else {
activities = await db.select()
.from(retailActivityFeed)
.orderBy(desc(retailActivityFeed.createdAt))
.limit(limit);
}
res.json(activities);
} catch (error) {
console.error("Error fetching activity feed:", error);
res.status(500).json({ error: "Failed to fetch activity feed" });
}
});
router.post("/activity-feed/mark-read", async (req: Request, res: Response) => {
try {
const { ids } = req.body;
const user = (req as any).user;
const tenantId = user?.tenantId;
// Validate input
if (!ids || !Array.isArray(ids) || ids.length === 0) {
return res.status(400).json({ error: "Invalid ids array" });
}
// Validate all ids are numbers
const validIds = ids.filter(id => typeof id === 'number' && Number.isInteger(id));
if (validIds.length === 0) {
return res.status(400).json({ error: "No valid ids provided" });
}
// Scope by tenant if available
if (tenantId) {
await db.update(retailActivityFeed)
.set({ isRead: true })
.where(and(
inArray(retailActivityFeed.id, validIds),
or(
eq(retailActivityFeed.tenantId, tenantId),
sql`${retailActivityFeed.tenantId} IS NULL`
)
));
} else {
await db.update(retailActivityFeed)
.set({ isRead: true })
.where(inArray(retailActivityFeed.id, validIds));
}
res.json({ success: true });
} catch (error) {
console.error("Error marking activities as read:", error);
res.status(500).json({ error: "Failed to mark activities as read" });
}
});
// ========== FORMAS DE PAGAMENTO ==========
router.get("/payment-methods", async (req: Request, res: Response) => {
try {
const methods = await db.select().from(retailPaymentMethods).orderBy(asc(retailPaymentMethods.name));
res.json(methods);
} catch (error) {
console.error("Error fetching payment methods:", error);
res.status(500).json({ error: "Failed to fetch payment methods" });
}
});
router.post("/payment-methods", async (req: Request, res: Response) => {
try {
const user = (req as any).user;
const [method] = await db.insert(retailPaymentMethods)
.values({ ...req.body, tenantId: user?.tenantId })
.returning();
res.json(method);
} catch (error) {
console.error("Error creating payment method:", error);
res.status(500).json({ error: "Failed to create payment method" });
}
});
router.put("/payment-methods/:id", async (req: Request, res: Response) => {
try {
const [method] = await db.update(retailPaymentMethods)
.set({ ...req.body, updatedAt: new Date() })
.where(eq(retailPaymentMethods.id, parseInt(req.params.id)))
.returning();
res.json(method);
} catch (error) {
console.error("Error updating payment method:", error);
res.status(500).json({ error: "Failed to update payment method" });
}
});
router.delete("/payment-methods/:id", async (req: Request, res: Response) => {
try {
await db.delete(retailPaymentMethods).where(eq(retailPaymentMethods.id, parseInt(req.params.id)));
res.json({ success: true });
} catch (error) {
console.error("Error deleting payment method:", error);
res.status(500).json({ error: "Failed to delete payment method" });
}
});
// ========== VENDEDORES ==========
router.get("/sellers", async (req: Request, res: Response) => {
try {
const sellers = await db.select().from(retailSellers).orderBy(asc(retailSellers.name));
res.json(sellers);
} catch (error) {
console.error("Error fetching sellers:", error);
res.status(500).json({ error: "Failed to fetch sellers" });
}
});
router.post("/sellers", async (req: Request, res: Response) => {
try {
const user = (req as any).user;
const [seller] = await db.insert(retailSellers)
.values({ ...req.body, tenantId: user?.tenantId })
.returning();
res.json(seller);
} catch (error) {
console.error("Error creating seller:", error);
res.status(500).json({ error: "Failed to create seller" });
}
});
router.put("/sellers/:id", async (req: Request, res: Response) => {
try {
const [seller] = await db.update(retailSellers)
.set({ ...req.body, updatedAt: new Date() })
.where(eq(retailSellers.id, parseInt(req.params.id)))
.returning();
res.json(seller);
} catch (error) {
console.error("Error updating seller:", error);
res.status(500).json({ error: "Failed to update seller" });
}
});
router.delete("/sellers/:id", async (req: Request, res: Response) => {
try {
await db.delete(retailSellers).where(eq(retailSellers.id, parseInt(req.params.id)));
res.json({ success: true });
} catch (error) {
console.error("Error deleting seller:", error);
res.status(500).json({ error: "Failed to delete seller" });
}
});
// ========== PLANOS DE COMISSÃO ==========
router.get("/commission-plans", async (req: Request, res: Response) => {
try {
const plans = await db.select().from(retailCommissionPlans).orderBy(asc(retailCommissionPlans.name));
res.json(plans);
} catch (error) {
console.error("Error fetching commission plans:", error);
res.status(500).json({ error: "Failed to fetch commission plans" });
}
});
router.post("/commission-plans", async (req: Request, res: Response) => {
try {
const user = (req as any).user;
const [plan] = await db.insert(retailCommissionPlans)
.values({ ...req.body, tenantId: user?.tenantId })
.returning();
res.json(plan);
} catch (error) {
console.error("Error creating commission plan:", error);
res.status(500).json({ error: "Failed to create commission plan" });
}
});
router.put("/commission-plans/:id", async (req: Request, res: Response) => {
try {
const [plan] = await db.update(retailCommissionPlans)
.set({ ...req.body, updatedAt: new Date() })
.where(eq(retailCommissionPlans.id, parseInt(req.params.id)))
.returning();
res.json(plan);
} catch (error) {
console.error("Error updating commission plan:", error);
res.status(500).json({ error: "Failed to update commission plan" });
}
});
router.delete("/commission-plans/:id", async (req: Request, res: Response) => {
try {
await db.delete(retailCommissionPlans).where(eq(retailCommissionPlans.id, parseInt(req.params.id)));
res.json({ success: true });
} catch (error) {
console.error("Error deleting commission plan:", error);
res.status(500).json({ error: "Failed to delete commission plan" });
}
});
// ========== TABELAS DE PREÇO ==========
router.get("/price-tables", async (req: Request, res: Response) => {
try {
const tables = await db.select().from(retailPriceTables).orderBy(asc(retailPriceTables.name));
res.json(tables);
} catch (error) {
console.error("Error fetching price tables:", error);
res.status(500).json({ error: "Failed to fetch price tables" });
}
});
router.post("/price-tables", async (req: Request, res: Response) => {
try {
const user = (req as any).user;
const [table] = await db.insert(retailPriceTables)
.values({ ...req.body, tenantId: user?.tenantId })
.returning();
res.json(table);
} catch (error) {
console.error("Error creating price table:", error);
res.status(500).json({ error: "Failed to create price table" });
}
});
router.put("/price-tables/:id", async (req: Request, res: Response) => {
try {
const [table] = await db.update(retailPriceTables)
.set({ ...req.body, updatedAt: new Date() })
.where(eq(retailPriceTables.id, parseInt(req.params.id)))
.returning();
res.json(table);
} catch (error) {
console.error("Error updating price table:", error);
res.status(500).json({ error: "Failed to update price table" });
}
});
router.delete("/price-tables/:id", async (req: Request, res: Response) => {
try {
await db.delete(retailPriceTables).where(eq(retailPriceTables.id, parseInt(req.params.id)));
res.json({ success: true });
} catch (error) {
console.error("Error deleting price table:", error);
res.status(500).json({ error: "Failed to delete price table" });
}
});
// ========== PROMOÇÕES ==========
router.get("/promotions", async (req: Request, res: Response) => {
try {
const promos = await db.select().from(retailPromotions).orderBy(desc(retailPromotions.createdAt));
res.json(promos);
} catch (error) {
console.error("Error fetching promotions:", error);
res.status(500).json({ error: "Failed to fetch promotions" });
}
});
router.post("/promotions", async (req: Request, res: Response) => {
try {
const user = (req as any).user;
const [promo] = await db.insert(retailPromotions)
.values({ ...req.body, tenantId: user?.tenantId })
.returning();
res.json(promo);
} catch (error) {
console.error("Error creating promotion:", error);
res.status(500).json({ error: "Failed to create promotion" });
}
});
router.put("/promotions/:id", async (req: Request, res: Response) => {
try {
const [promo] = await db.update(retailPromotions)
.set({ ...req.body, updatedAt: new Date() })
.where(eq(retailPromotions.id, parseInt(req.params.id)))
.returning();
res.json(promo);
} catch (error) {
console.error("Error updating promotion:", error);
res.status(500).json({ error: "Failed to update promotion" });
}
});
router.delete("/promotions/:id", async (req: Request, res: Response) => {
try {
await db.delete(retailPromotions).where(eq(retailPromotions.id, parseInt(req.params.id)));
res.json({ success: true });
} catch (error) {
console.error("Error deleting promotion:", error);
res.status(500).json({ error: "Failed to delete promotion" });
}
});
// ========== PRODUCT TYPES (Tipos de Dispositivos e Acessórios) ==========
router.get("/product-types", async (req: Request, res: Response) => {
try {
const { category } = req.query;
const conditions = [];
if (category) conditions.push(eq(retailProductTypes.category, category as string));
const types = conditions.length > 0
? await db.select().from(retailProductTypes).where(and(...conditions)).orderBy(retailProductTypes.name)
: await db.select().from(retailProductTypes).orderBy(retailProductTypes.name);
res.json(types);
} catch (error) {
console.error("Error fetching product types:", error);
res.status(500).json({ error: "Failed to fetch product types" });
}
});
router.get("/product-types/:id", async (req: Request, res: Response) => {
try {
const [type] = await db.select().from(retailProductTypes)
.where(eq(retailProductTypes.id, parseInt(req.params.id)));
if (!type) return res.status(404).json({ error: "Product type not found" });
res.json(type);
} catch (error) {
console.error("Error fetching product type:", error);
res.status(500).json({ error: "Failed to fetch product type" });
}
});
router.post("/product-types", async (req: Request, res: Response) => {
try {
const user = (req as any).user;
const [type] = await db.insert(retailProductTypes)
.values({ ...req.body, tenantId: user?.tenantId })
.returning();
await logActivity({
activityType: "product_type_created",
title: `Tipo de produto criado`,
description: `Tipo de produto "${type.name}" criado`,
entityType: "product_type",
entityId: type.id,
tenantId: user?.tenantId,
metadata: { category: type.category }
});
res.json(type);
} catch (error) {
console.error("Error creating product type:", error);
res.status(500).json({ error: "Failed to create product type" });
}
});
router.put("/product-types/:id", async (req: Request, res: Response) => {
try {
const [type] = await db.update(retailProductTypes)
.set({ ...req.body, updatedAt: sql`CURRENT_TIMESTAMP` })
.where(eq(retailProductTypes.id, parseInt(req.params.id)))
.returning();
res.json(type);
} catch (error) {
console.error("Error updating product type:", error);
res.status(500).json({ error: "Failed to update product type" });
}
});
router.delete("/product-types/:id", async (req: Request, res: Response) => {
try {
await db.delete(retailProductTypes).where(eq(retailProductTypes.id, parseInt(req.params.id)));
res.json({ success: true });
} catch (error) {
console.error("Error deleting product type:", error);
res.status(500).json({ error: "Failed to delete product type" });
}
});
// ========== STORES ==========
router.get("/stores", async (req: Request, res: Response) => {
try {
const stores = await db.select().from(retailStores).orderBy(desc(retailStores.createdAt));
res.json(stores);
} catch (error) {
console.error("Error fetching stores:", error);
res.status(500).json({ error: "Failed to fetch stores" });
}
});
router.post("/stores", async (req: Request, res: Response) => {
try {
const [store] = await db.insert(retailStores).values(req.body).returning();
res.json(store);
} catch (error) {
console.error("Error creating store:", error);
res.status(500).json({ error: "Failed to create store" });
}
});
router.put("/stores/:id", async (req: Request, res: Response) => {
try {
const [store] = await db.update(retailStores)
.set({ ...req.body, updatedAt: sql`CURRENT_TIMESTAMP` })
.where(eq(retailStores.id, parseInt(req.params.id)))
.returning();
res.json(store);
} catch (error) {
console.error("Error updating store:", error);
res.status(500).json({ error: "Failed to update store" });
}
});
// ========== WAREHOUSES ==========
router.get("/warehouses", async (req: Request, res: Response) => {
try {
const warehouses = await db.select().from(retailWarehouses).orderBy(desc(retailWarehouses.createdAt));
res.json(warehouses);
} catch (error) {
console.error("Error fetching warehouses:", error);
res.status(500).json({ error: "Failed to fetch warehouses" });
}
});
router.post("/warehouses", async (req: Request, res: Response) => {
try {
const [warehouse] = await db.insert(retailWarehouses).values(req.body).returning();
res.json(warehouse);
} catch (error) {
console.error("Error creating warehouse:", error);
res.status(500).json({ error: "Failed to create warehouse" });
}
});
router.put("/warehouses/:id", async (req: Request, res: Response) => {
try {
const [warehouse] = await db.update(retailWarehouses)
.set({ ...req.body, updatedAt: sql`CURRENT_TIMESTAMP` })
.where(eq(retailWarehouses.id, parseInt(req.params.id)))
.returning();
res.json(warehouse);
} catch (error) {
console.error("Error updating warehouse:", error);
res.status(500).json({ error: "Failed to update warehouse" });
}
});
router.delete("/warehouses/:id", async (req: Request, res: Response) => {
try {
await db.delete(retailWarehouses).where(eq(retailWarehouses.id, parseInt(req.params.id)));
res.json({ success: true });
} catch (error) {
console.error("Error deleting warehouse:", error);
res.status(500).json({ error: "Failed to delete warehouse" });
}
});
// ========== WAREHOUSE STOCK (Saldos por Depósito) ==========
router.get("/warehouse-stock", async (req: Request, res: Response) => {
try {
const { warehouseId, productId } = req.query;
let conditions = [];
if (warehouseId) conditions.push(eq(retailWarehouseStock.warehouseId, parseInt(warehouseId as string)));
if (productId) conditions.push(eq(retailWarehouseStock.productId, parseInt(productId as string)));
const stock = conditions.length > 0
? await db.select().from(retailWarehouseStock).where(and(...conditions))
: await db.select().from(retailWarehouseStock);
res.json(stock);
} catch (error) {
console.error("Error fetching warehouse stock:", error);
res.status(500).json({ error: "Failed to fetch warehouse stock" });
}
});
router.get("/warehouse-stock/:warehouseId/summary", async (req: Request, res: Response) => {
try {
const warehouseId = parseInt(req.params.warehouseId);
const stock = await db.select({
id: retailWarehouseStock.id,
productId: retailWarehouseStock.productId,
productName: products.name,
productCode: products.code,
quantity: retailWarehouseStock.quantity,
reservedQuantity: retailWarehouseStock.reservedQuantity,
availableQuantity: retailWarehouseStock.availableQuantity,
minStock: retailWarehouseStock.minStock,
maxStock: retailWarehouseStock.maxStock,
lastMovementAt: retailWarehouseStock.lastMovementAt,
})
.from(retailWarehouseStock)
.leftJoin(products, eq(retailWarehouseStock.productId, products.id))
.where(eq(retailWarehouseStock.warehouseId, warehouseId));
res.json(stock);
} catch (error) {
console.error("Error fetching warehouse stock summary:", error);
res.status(500).json({ error: "Failed to fetch warehouse stock summary" });
}
});
// ========== STOCK MOVEMENTS (Movimentações de Estoque) ==========
router.get("/stock-movements", async (req: Request, res: Response) => {
try {
const { warehouseId, productId, movementType, limit = "50" } = req.query;
let conditions = [];
if (warehouseId) conditions.push(eq(retailStockMovements.warehouseId, parseInt(warehouseId as string)));
if (productId) conditions.push(eq(retailStockMovements.productId, parseInt(productId as string)));
if (movementType) conditions.push(eq(retailStockMovements.movementType, movementType as string));
const movements = conditions.length > 0
? await db.select().from(retailStockMovements).where(and(...conditions)).orderBy(desc(retailStockMovements.createdAt)).limit(parseInt(limit as string))
: await db.select().from(retailStockMovements).orderBy(desc(retailStockMovements.createdAt)).limit(parseInt(limit as string));
res.json(movements);
} catch (error) {
console.error("Error fetching stock movements:", error);
res.status(500).json({ error: "Failed to fetch stock movements" });
}
});
router.post("/stock-movements", async (req: Request, res: Response) => {
try {
const { warehouseId, productId, movementType, operationType, quantity, unitCost, supplierId, referenceNumber, notes, serials } = req.body;
// Buscar saldo atual
let [currentStock] = await db.select().from(retailWarehouseStock)
.where(and(
eq(retailWarehouseStock.warehouseId, warehouseId),
eq(retailWarehouseStock.productId, productId)
));
const previousStock = currentStock ? parseFloat(currentStock.quantity) : 0;
const movementQty = parseFloat(quantity);
const newStock = movementType === "entry" || movementType === "transfer_in"
? previousStock + movementQty
: previousStock - movementQty;
// Inserir movimentação
const [movement] = await db.insert(retailStockMovements).values({
warehouseId,
productId,
movementType,
operationType,
quantity: quantity.toString(),
previousStock: previousStock.toString(),
newStock: newStock.toString(),
unitCost: unitCost?.toString(),
totalCost: unitCost ? (movementQty * parseFloat(unitCost)).toString() : null,
supplierId,
referenceNumber,
notes,
userId: (req.user as any)?.id,
}).returning();
// Atualizar ou criar saldo de estoque
if (currentStock) {
await db.update(retailWarehouseStock)
.set({
quantity: newStock.toString(),
availableQuantity: (newStock - parseFloat(currentStock.reservedQuantity || "0")).toString(),
lastMovementAt: sql`CURRENT_TIMESTAMP`,
updatedAt: sql`CURRENT_TIMESTAMP`
})
.where(eq(retailWarehouseStock.id, currentStock.id));
} else {
await db.insert(retailWarehouseStock).values({
warehouseId,
productId,
quantity: newStock.toString(),
availableQuantity: newStock.toString(),
reservedQuantity: "0",
lastMovementAt: sql`CURRENT_TIMESTAMP`,
});
}
// Inserir números de série/IMEI se fornecidos
if (serials && Array.isArray(serials) && serials.length > 0) {
const serialRecords = serials.map((s: any) => ({
productId,
warehouseId,
serialNumber: s.serialNumber,
imei: s.imei,
imei2: s.imei2,
status: movementType === "entry" ? "in_stock" : "sold",
acquisitionCost: unitCost?.toString(),
movementId: movement.id,
purchaseNfeNumber: referenceNumber,
}));
await db.insert(retailProductSerials).values(serialRecords);
}
res.json(movement);
} catch (error) {
console.error("Error creating stock movement:", error);
res.status(500).json({ error: "Failed to create stock movement" });
}
});
// ========== PRODUCT SERIALS (Números de Série/IMEI) ==========
router.get("/product-serials", async (req: Request, res: Response) => {
try {
const { productId, warehouseId, status, search } = req.query;
let conditions = [];
if (productId) conditions.push(eq(retailProductSerials.productId, parseInt(productId as string)));
if (warehouseId) conditions.push(eq(retailProductSerials.warehouseId, parseInt(warehouseId as string)));
if (status) conditions.push(eq(retailProductSerials.status, status as string));
if (search) {
conditions.push(or(
ilike(retailProductSerials.imei, `%${search}%`),
ilike(retailProductSerials.serialNumber, `%${search}%`)
));
}
const serials = conditions.length > 0
? await db.select().from(retailProductSerials).where(and(...conditions)).orderBy(desc(retailProductSerials.createdAt))
: await db.select().from(retailProductSerials).orderBy(desc(retailProductSerials.createdAt));
res.json(serials);
} catch (error) {
console.error("Error fetching product serials:", error);
res.status(500).json({ error: "Failed to fetch product serials" });
}
});
router.post("/product-serials", async (req: Request, res: Response) => {
try {
const [serial] = await db.insert(retailProductSerials).values(req.body).returning();
res.json(serial);
} catch (error) {
console.error("Error creating product serial:", error);
res.status(500).json({ error: "Failed to create product serial" });
}
});
router.put("/product-serials/:id", async (req: Request, res: Response) => {
try {
const [serial] = await db.update(retailProductSerials)
.set({ ...req.body, updatedAt: sql`CURRENT_TIMESTAMP` })
.where(eq(retailProductSerials.id, parseInt(req.params.id)))
.returning();
res.json(serial);
} catch (error) {
console.error("Error updating product serial:", error);
res.status(500).json({ error: "Failed to update product serial" });
}
});
// ========== STOCK TRANSFERS (Transferências entre Depósitos) ==========
router.get("/stock-transfers", async (req: Request, res: Response) => {
try {
const { status, sourceWarehouseId, destinationWarehouseId } = req.query;
let conditions = [];
if (status) conditions.push(eq(retailStockTransfers.status, status as string));
if (sourceWarehouseId) conditions.push(eq(retailStockTransfers.sourceWarehouseId, parseInt(sourceWarehouseId as string)));
if (destinationWarehouseId) conditions.push(eq(retailStockTransfers.destinationWarehouseId, parseInt(destinationWarehouseId as string)));
const transfers = conditions.length > 0
? await db.select().from(retailStockTransfers).where(and(...conditions)).orderBy(desc(retailStockTransfers.createdAt))
: await db.select().from(retailStockTransfers).orderBy(desc(retailStockTransfers.createdAt));
res.json(transfers);
} catch (error) {
console.error("Error fetching stock transfers:", error);
res.status(500).json({ error: "Failed to fetch stock transfers" });
}
});
router.get("/stock-transfers/:id", async (req: Request, res: Response) => {
try {
const [transfer] = await db.select().from(retailStockTransfers)
.where(eq(retailStockTransfers.id, parseInt(req.params.id)));
if (!transfer) return res.status(404).json({ error: "Transfer not found" });
const items = await db.select({
id: retailStockTransferItems.id,
productId: retailStockTransferItems.productId,
productName: products.name,
productCode: products.code,
requestedQuantity: retailStockTransferItems.requestedQuantity,
transferredQuantity: retailStockTransferItems.transferredQuantity,
receivedQuantity: retailStockTransferItems.receivedQuantity,
notes: retailStockTransferItems.notes,
})
.from(retailStockTransferItems)
.leftJoin(products, eq(retailStockTransferItems.productId, products.id))
.where(eq(retailStockTransferItems.transferId, transfer.id));
res.json({ ...transfer, items });
} catch (error) {
console.error("Error fetching stock transfer:", error);
res.status(500).json({ error: "Failed to fetch stock transfer" });
}
});
router.post("/stock-transfers", async (req: Request, res: Response) => {
try {
const { sourceWarehouseId, destinationWarehouseId, items, notes } = req.body;
// Gerar número da transferência
const transferNumber = `TRF-${Date.now().toString(36).toUpperCase()}`;
const [transfer] = await db.insert(retailStockTransfers).values({
sourceWarehouseId,
destinationWarehouseId,
transferNumber,
notes,
requestedBy: (req.user as any)?.id,
status: "pending",
}).returning();
// Inserir itens da transferência
if (items && Array.isArray(items)) {
for (const item of items) {
await db.insert(retailStockTransferItems).values({
transferId: transfer.id,
productId: item.productId,
requestedQuantity: item.quantity.toString(),
});
}
}
res.json(transfer);
} catch (error) {
console.error("Error creating stock transfer:", error);
res.status(500).json({ error: "Failed to create stock transfer" });
}
});
router.put("/stock-transfers/:id/status", async (req: Request, res: Response) => {
try {
const { status } = req.body;
const transferId = parseInt(req.params.id);
const userId = (req.user as any)?.id;
const updateData: any = { status };
if (status === "in_transit") {
updateData.approvedBy = userId;
updateData.approvedAt = sql`CURRENT_TIMESTAMP`;
} else if (status === "completed") {
updateData.completedBy = userId;
updateData.completedAt = sql`CURRENT_TIMESTAMP`;
// Processar movimentações de estoque
const [transfer] = await db.select().from(retailStockTransfers).where(eq(retailStockTransfers.id, transferId));
const items = await db.select().from(retailStockTransferItems).where(eq(retailStockTransferItems.transferId, transferId));
for (const item of items) {
const qty = parseFloat(item.requestedQuantity);
// Saída do depósito de origem
await db.insert(retailStockMovements).values({
warehouseId: transfer.sourceWarehouseId,
productId: item.productId,
movementType: "transfer_out",
operationType: "transfer",
quantity: qty.toString(),
referenceType: "transfer",
referenceId: transferId,
referenceNumber: transfer.transferNumber,
userId,
});
// Entrada no depósito de destino
await db.insert(retailStockMovements).values({
warehouseId: transfer.destinationWarehouseId,
productId: item.productId,
movementType: "transfer_in",
operationType: "transfer",
quantity: qty.toString(),
referenceType: "transfer",
referenceId: transferId,
referenceNumber: transfer.transferNumber,
userId,
});
// Atualizar saldos
// Origem
const [sourceStock] = await db.select().from(retailWarehouseStock)
.where(and(eq(retailWarehouseStock.warehouseId, transfer.sourceWarehouseId), eq(retailWarehouseStock.productId, item.productId)));
if (sourceStock) {
const newQty = parseFloat(sourceStock.quantity) - qty;
await db.update(retailWarehouseStock)
.set({ quantity: newQty.toString(), availableQuantity: newQty.toString(), lastMovementAt: sql`CURRENT_TIMESTAMP`, updatedAt: sql`CURRENT_TIMESTAMP` })
.where(eq(retailWarehouseStock.id, sourceStock.id));
}
// Destino
let [destStock] = await db.select().from(retailWarehouseStock)
.where(and(eq(retailWarehouseStock.warehouseId, transfer.destinationWarehouseId), eq(retailWarehouseStock.productId, item.productId)));
if (destStock) {
const newQty = parseFloat(destStock.quantity) + qty;
await db.update(retailWarehouseStock)
.set({ quantity: newQty.toString(), availableQuantity: newQty.toString(), lastMovementAt: sql`CURRENT_TIMESTAMP`, updatedAt: sql`CURRENT_TIMESTAMP` })
.where(eq(retailWarehouseStock.id, destStock.id));
} else {
await db.insert(retailWarehouseStock).values({
warehouseId: transfer.destinationWarehouseId,
productId: item.productId,
quantity: qty.toString(),
availableQuantity: qty.toString(),
reservedQuantity: "0",
lastMovementAt: sql`CURRENT_TIMESTAMP`,
});
}
}
}
const [updated] = await db.update(retailStockTransfers)
.set(updateData)
.where(eq(retailStockTransfers.id, transferId))
.returning();
res.json(updated);
} catch (error) {
console.error("Error updating stock transfer status:", error);
res.status(500).json({ error: "Failed to update stock transfer status" });
}
});
// ========== INVENTORIES (Inventários) ==========
router.get("/inventories", async (req: Request, res: Response) => {
try {
const { warehouseId, status } = req.query;
let conditions = [];
if (warehouseId) conditions.push(eq(retailInventories.warehouseId, parseInt(warehouseId as string)));
if (status) conditions.push(eq(retailInventories.status, status as string));
const inventories = conditions.length > 0
? await db.select().from(retailInventories).where(and(...conditions)).orderBy(desc(retailInventories.createdAt))
: await db.select().from(retailInventories).orderBy(desc(retailInventories.createdAt));
res.json(inventories);
} catch (error) {
console.error("Error fetching inventories:", error);
res.status(500).json({ error: "Failed to fetch inventories" });
}
});
router.post("/inventories", async (req: Request, res: Response) => {
try {
const { warehouseId, type, notes } = req.body;
const inventoryNumber = `INV-${Date.now().toString(36).toUpperCase()}`;
const [inventory] = await db.insert(retailInventories).values({
warehouseId,
type: type || "full",
inventoryNumber,
status: "open",
notes,
createdBy: (req.user as any)?.id,
startedAt: sql`CURRENT_TIMESTAMP`,
}).returning();
// Carregar itens do estoque atual para contagem
const stock = await db.select().from(retailWarehouseStock)
.where(eq(retailWarehouseStock.warehouseId, warehouseId));
for (const item of stock) {
await db.insert(retailInventoryItems).values({
inventoryId: inventory.id,
productId: item.productId,
systemQuantity: item.quantity,
});
}
res.json(inventory);
} catch (error) {
console.error("Error creating inventory:", error);
res.status(500).json({ error: "Failed to create inventory" });
}
});
router.get("/inventories/:id", async (req: Request, res: Response) => {
try {
const [inventory] = await db.select().from(retailInventories)
.where(eq(retailInventories.id, parseInt(req.params.id)));
if (!inventory) return res.status(404).json({ error: "Inventory not found" });
const items = await db.select({
id: retailInventoryItems.id,
productId: retailInventoryItems.productId,
productName: products.name,
productCode: products.code,
systemQuantity: retailInventoryItems.systemQuantity,
countedQuantity: retailInventoryItems.countedQuantity,
difference: retailInventoryItems.difference,
adjustmentApplied: retailInventoryItems.adjustmentApplied,
})
.from(retailInventoryItems)
.leftJoin(products, eq(retailInventoryItems.productId, products.id))
.where(eq(retailInventoryItems.inventoryId, inventory.id));
res.json({ ...inventory, items });
} catch (error) {
console.error("Error fetching inventory:", error);
res.status(500).json({ error: "Failed to fetch inventory" });
}
});
router.put("/inventories/:id/count", async (req: Request, res: Response) => {
try {
const { items } = req.body;
const userId = (req.user as any)?.id;
for (const item of items) {
const countedQty = parseFloat(item.countedQuantity);
const systemQty = parseFloat(item.systemQuantity || 0);
await db.update(retailInventoryItems)
.set({
countedQuantity: countedQty.toString(),
difference: (countedQty - systemQty).toString(),
countedBy: userId,
countedAt: sql`CURRENT_TIMESTAMP`,
})
.where(eq(retailInventoryItems.id, item.id));
}
await db.update(retailInventories)
.set({ status: "counting" })
.where(eq(retailInventories.id, parseInt(req.params.id)));
res.json({ success: true });
} catch (error) {
console.error("Error updating inventory count:", error);
res.status(500).json({ error: "Failed to update inventory count" });
}
});
router.put("/inventories/:id/apply", async (req: Request, res: Response) => {
try {
const inventoryId = parseInt(req.params.id);
const userId = (req.user as any)?.id;
const [inventory] = await db.select().from(retailInventories).where(eq(retailInventories.id, inventoryId));
const items = await db.select().from(retailInventoryItems).where(eq(retailInventoryItems.inventoryId, inventoryId));
for (const item of items) {
if (item.countedQuantity !== null && !item.adjustmentApplied) {
const diff = parseFloat(item.difference || "0");
if (diff !== 0) {
// Criar movimentação de ajuste
await db.insert(retailStockMovements).values({
warehouseId: inventory.warehouseId,
productId: item.productId,
movementType: "adjustment",
operationType: "inventory_adjustment",
quantity: Math.abs(diff).toString(),
referenceType: "inventory",
referenceId: inventoryId,
referenceNumber: inventory.inventoryNumber,
notes: `Ajuste de inventário: ${diff > 0 ? "+" : ""}${diff}`,
userId,
});
// Atualizar saldo
const [stock] = await db.select().from(retailWarehouseStock)
.where(and(eq(retailWarehouseStock.warehouseId, inventory.warehouseId), eq(retailWarehouseStock.productId, item.productId)));
if (stock) {
await db.update(retailWarehouseStock)
.set({
quantity: item.countedQuantity,
availableQuantity: item.countedQuantity,
lastInventoryAt: sql`CURRENT_TIMESTAMP`,
lastMovementAt: sql`CURRENT_TIMESTAMP`,
updatedAt: sql`CURRENT_TIMESTAMP`
})
.where(eq(retailWarehouseStock.id, stock.id));
}
}
await db.update(retailInventoryItems)
.set({ adjustmentApplied: true })
.where(eq(retailInventoryItems.id, item.id));
}
}
await db.update(retailInventories)
.set({ status: "completed", completedAt: sql`CURRENT_TIMESTAMP`, completedBy: userId })
.where(eq(retailInventories.id, inventoryId));
res.json({ success: true });
} catch (error) {
console.error("Error applying inventory adjustments:", error);
res.status(500).json({ error: "Failed to apply inventory adjustments" });
}
});
// ========== MOBILE DEVICES ==========
router.get("/devices", async (req: Request, res: Response) => {
try {
const { search, status, storeId } = req.query;
let query = db.select().from(mobileDevices);
const conditions = [];
if (search) {
conditions.push(or(
ilike(mobileDevices.imei, `%${search}%`),
ilike(mobileDevices.brand, `%${search}%`),
ilike(mobileDevices.model, `%${search}%`)
));
}
if (status) {
conditions.push(eq(mobileDevices.status, status as string));
}
if (storeId) {
conditions.push(eq(mobileDevices.storeId, parseInt(storeId as string)));
}
const devices = conditions.length > 0
? await query.where(and(...conditions)).orderBy(desc(mobileDevices.createdAt))
: await query.orderBy(desc(mobileDevices.createdAt));
res.json(devices);
} catch (error) {
console.error("Error fetching devices:", error);
res.status(500).json({ error: "Failed to fetch devices" });
}
});
router.get("/devices/:id", async (req: Request, res: Response) => {
try {
const [device] = await db.select().from(mobileDevices)
.where(eq(mobileDevices.id, parseInt(req.params.id)));
if (!device) return res.status(404).json({ error: "Device not found" });
res.json(device);
} catch (error) {
console.error("Error fetching device:", error);
res.status(500).json({ error: "Failed to fetch device" });
}
});
router.get("/devices/imei/:imei", async (req: Request, res: Response) => {
try {
const [device] = await db.select().from(mobileDevices)
.where(eq(mobileDevices.imei, req.params.imei));
if (!device) return res.status(404).json({ error: "Device not found" });
res.json(device);
} catch (error) {
console.error("Error fetching device by IMEI:", error);
res.status(500).json({ error: "Failed to fetch device" });
}
});
router.post("/devices", async (req: Request, res: Response) => {
try {
if (req.body.imei) {
const existing = await db.select({ id: mobileDevices.id }).from(mobileDevices)
.where(eq(mobileDevices.imei, req.body.imei))
.limit(1);
if (existing.length > 0) {
return res.status(400).json({ error: "IMEI já cadastrado no sistema. Cada aparelho deve ter um IMEI único." });
}
}
const [device] = await db.insert(mobileDevices).values(req.body).returning();
await db.insert(deviceHistory).values({
deviceId: device.id,
imei: device.imei,
eventType: "received",
toLocation: req.body.storeId ? `Store ${req.body.storeId}` : `Warehouse ${req.body.warehouseId}`,
createdBy: (req as any).user?.id
});
res.json(device);
} catch (error) {
console.error("Error creating device:", error);
res.status(500).json({ error: "Failed to create device" });
}
});
router.put("/devices/:id", async (req: Request, res: Response) => {
try {
const updateData: Record<string, any> = { ...req.body, updatedAt: sql`CURRENT_TIMESTAMP` };
if (updateData.purchasePrice) updateData.purchasePrice = String(updateData.purchasePrice);
if (updateData.sellingPrice) updateData.sellingPrice = String(updateData.sellingPrice);
if (updateData.warrantyEndDate) updateData.warrantyEndDate = updateData.warrantyEndDate;
const [device] = await db.update(mobileDevices)
.set(updateData)
.where(eq(mobileDevices.id, parseInt(req.params.id)))
.returning();
res.json(device);
} catch (error) {
console.error("Error updating device:", error);
res.status(500).json({ error: "Failed to update device" });
}
});
// ========== DEVICE EVALUATIONS (Trade-In) ==========
router.get("/evaluations", async (req: Request, res: Response) => {
try {
const evaluations = await db.select().from(deviceEvaluations)
.orderBy(desc(deviceEvaluations.createdAt));
res.json(evaluations);
} catch (error) {
console.error("Error fetching evaluations:", error);
res.status(500).json({ error: "Failed to fetch evaluations" });
}
});
router.post("/evaluations", async (req: Request, res: Response) => {
try {
console.log("Creating evaluation with body:", JSON.stringify(req.body, null, 2));
const estimatedValue = calculateTradeInValue(req.body);
console.log("Calculated estimatedValue:", estimatedValue);
const [evaluation] = await db.insert(deviceEvaluations)
.values({ ...req.body, estimatedValue })
.returning();
console.log("Evaluation created:", evaluation.id);
// Log activity with user context
const user = (req as any).user;
await logActivity({
activityType: "evaluation",
entityType: "device_evaluation",
entityId: evaluation.id,
title: `Nova avaliação Trade-In: ${req.body.brand} ${req.body.model}`,
description: `IMEI: ${req.body.imei} - Cliente: ${req.body.customerName || 'N/A'} - Valor: R$ ${estimatedValue}`,
severity: "info",
metadata: { brand: req.body.brand, model: req.body.model, imei: req.body.imei, estimatedValue },
tenantId: user?.tenantId,
storeId: req.body.storeId,
createdBy: user?.id,
createdByName: user?.name || user?.username
});
res.json(evaluation);
} catch (error: any) {
console.error("Error creating evaluation:", error);
console.error("Error message:", error.message);
console.error("Error details:", error.detail);
res.status(500).json({ error: "Failed to create evaluation", message: error.message });
}
});
router.put("/evaluations/:id/approve", async (req: Request, res: Response) => {
try {
const { productId } = req.body; // Produto existente para relacionar (opcional)
const [evaluation] = await db.select().from(deviceEvaluations)
.where(eq(deviceEvaluations.id, parseInt(req.params.id)));
if (!evaluation) return res.status(404).json({ error: "Evaluation not found" });
// Criar dispositivo com status pending_preparation e acquisitionType trade_in
const [device] = await db.insert(mobileDevices).values({
imei: evaluation.imei,
brand: evaluation.brand,
model: evaluation.model,
color: evaluation.color || undefined,
condition: "trade_in", // Não "used" - pode ser trade-in de produto novo
purchasePrice: evaluation.estimatedValue,
storeId: evaluation.storeId || undefined,
status: "pending_preparation", // Aguardando preparação
acquisitionType: "trade_in",
acquisitionCost: evaluation.estimatedValue,
relatedEvaluationId: evaluation.id,
productId: productId || undefined, // Relacionar a produto existente
personId: (evaluation as any).personId || undefined
}).returning();
// Criar O.S. interna de preparação/revisão
const osNumber = `OSI-${new Date().getFullYear()}-${String(Date.now()).slice(-6)}`;
const [serviceOrder] = await db.insert(serviceOrders).values({
orderNumber: osNumber,
storeId: evaluation.storeId || undefined,
deviceId: device.id,
imei: evaluation.imei,
brand: evaluation.brand,
model: evaluation.model,
customerName: "Ordem Interna - Trade-In",
serviceType: "internal_preparation",
origin: "trade_in",
issueDescription: `Preparação de aparelho Trade-In para estoque.\n\nCondições da avaliação:\n- Liga corretamente: ${evaluation.powerOn ? 'Sim' : 'Não'}\n- Problemas tela: ${evaluation.screenIssues ? 'Sim' : 'Não'}\n- Bateria: ${evaluation.batteryHealth || 'N/A'}%\n- Câmeras: ${evaluation.camerasWorking ? 'OK' : 'Verificar'}\n- Carregador: ${evaluation.hasCharger ? 'Incluso' : 'Não incluso'}\n- 3uTools OK: ${evaluation.toolsAnalysisOk ? 'Sim' : 'Não'}\n\nObservações: Realizar limpeza, teste completo, etiquetagem e liberar para estoque.`,
isInternal: true,
status: "open",
priority: "normal"
}).returning();
// Atualizar dispositivo com referência à O.S.
await db.update(mobileDevices)
.set({ relatedServiceOrderId: serviceOrder.id })
.where(eq(mobileDevices.id, device.id));
const [updated] = await db.update(deviceEvaluations)
.set({
approved: true,
status: "approved",
deviceId: device.id,
approvedBy: (req as any).user?.id,
updatedAt: new Date()
})
.where(eq(deviceEvaluations.id, parseInt(req.params.id)))
.returning();
// Buscar personId para criar o crédito - priorizar evaluation.personId
let personId = (evaluation as any).personId || null;
let customerCpf = null;
// Se tiver personId, buscar dados da pessoa
if (personId) {
const [person] = await db.select().from(persons)
.where(eq(persons.id, personId))
.limit(1);
if (person) {
customerCpf = person.cpfCnpj;
}
} else if (evaluation.customerName) {
// Fallback: buscar por nome se não tiver personId
const [person] = await db.select().from(persons)
.where(ilike(persons.fullName, evaluation.customerName))
.limit(1);
if (person) {
personId = person.id;
customerCpf = person.cpfCnpj;
}
}
// Criar crédito do cliente se tiver personId e valor válido > 0
let credit = null;
const estimatedValueNum = evaluation.estimatedValue ? parseFloat(evaluation.estimatedValue) : 0;
if (personId && estimatedValueNum > 0) {
[credit] = await db.insert(customerCredits).values({
storeId: evaluation.storeId || undefined,
personId: personId,
customerName: evaluation.customerName || "Cliente",
customerCpf: customerCpf || undefined,
amount: evaluation.estimatedValue!,
remainingAmount: evaluation.estimatedValue!,
origin: "trade_in",
originId: evaluation.id,
description: `Trade-In: ${evaluation.brand} ${evaluation.model} (IMEI: ${evaluation.imei})`,
status: "active",
createdBy: (req as any).user?.id
}).returning();
}
// Log activity with user context
const user = (req as any).user;
await logActivity({
activityType: "evaluation",
entityType: "device_evaluation",
entityId: evaluation.id,
title: `Trade-In APROVADO: ${evaluation.brand} ${evaluation.model}`,
description: `IMEI: ${evaluation.imei} - Valor: R$ ${evaluation.estimatedValue} - O.S. ${osNumber} criada`,
severity: "success",
metadata: { deviceId: device.id, serviceOrderId: serviceOrder.id, creditId: credit?.id },
tenantId: user?.tenantId,
storeId: evaluation.storeId || undefined,
createdBy: user?.id,
createdByName: user?.name || user?.username
});
res.json({ evaluation: updated, device, serviceOrder, credit });
} catch (error) {
console.error("Error approving evaluation:", error);
res.status(500).json({ error: "Failed to approve evaluation" });
}
});
// Update evaluation (edit checklist and value)
router.put("/evaluations/:id", async (req: Request, res: Response) => {
try {
const {
// Novo checklist completo
powerOn, powerOnNotes,
screenIssues, screenIssuesNotes,
screenSpots, screenSpotsNotes,
buttonsWorking, buttonsWorkingNotes,
wearMarks, wearMarksNotes,
wifiWorking, wifiWorkingNotes,
simWorking, simWorkingNotes,
mobileDataWorking, mobileDataWorkingNotes,
sensorsNfcWorking, sensorsNfcWorkingNotes,
biometricWorking, biometricWorkingNotes,
microphonesWorking, microphonesWorkingNotes,
earSpeakerWorking, earSpeakerWorkingNotes,
loudspeakerWorking, loudspeakerWorkingNotes,
chargingPortWorking, chargingPortWorkingNotes,
camerasWorking, camerasWorkingNotes,
flashWorking, flashWorkingNotes,
hasCharger, hasChargerNotes,
toolsAnalysisOk, toolsAnalysisNotes,
batteryHealth, batteryHealthNotes,
// Campos legados
screenCondition, bodyCondition, overallCondition, estimatedValue,
customerName, customerPhone, color
} = req.body;
const [evaluation] = await db.update(deviceEvaluations)
.set({
// Novo checklist
powerOn,
powerOnNotes,
screenIssues,
screenIssuesNotes,
screenSpots,
screenSpotsNotes,
buttonsWorking,
buttonsWorkingNotes,
wearMarks,
wearMarksNotes,
wifiWorking,
wifiWorkingNotes,
simWorking,
simWorkingNotes,
mobileDataWorking,
mobileDataWorkingNotes,
sensorsNfcWorking,
sensorsNfcWorkingNotes,
biometricWorking,
biometricWorkingNotes,
microphonesWorking,
microphonesWorkingNotes,
earSpeakerWorking,
earSpeakerWorkingNotes,
loudspeakerWorking,
loudspeakerWorkingNotes,
chargingPortWorking,
chargingPortWorkingNotes,
camerasWorking,
camerasWorkingNotes,
flashWorking,
flashWorkingNotes,
hasCharger,
hasChargerNotes,
toolsAnalysisOk,
toolsAnalysisNotes,
batteryHealth,
batteryHealthNotes,
// Legados
screenCondition,
bodyCondition,
overallCondition,
estimatedValue: estimatedValue?.toString(),
customerName,
customerPhone,
color,
updatedAt: new Date()
})
.where(eq(deviceEvaluations.id, parseInt(req.params.id)))
.returning();
res.json(evaluation);
} catch (error) {
console.error("Error updating evaluation:", error);
res.status(500).json({ error: "Failed to update evaluation" });
}
});
router.put("/evaluations/:id/reject", async (req: Request, res: Response) => {
try {
const [evaluation] = await db.update(deviceEvaluations)
.set({
approved: false,
status: "rejected",
rejectionReason: req.body.reason,
updatedAt: new Date()
})
.where(eq(deviceEvaluations.id, parseInt(req.params.id)))
.returning();
// Log activity with user context
const user = (req as any).user;
await logActivity({
activityType: "evaluation",
entityType: "device_evaluation",
entityId: evaluation.id,
title: `Trade-In REJEITADO: ${evaluation.brand} ${evaluation.model}`,
description: `IMEI: ${evaluation.imei} - Motivo: ${req.body.reason || 'Não especificado'}`,
severity: "warning",
metadata: { reason: req.body.reason },
tenantId: user?.tenantId,
storeId: evaluation.storeId || undefined,
createdBy: user?.id,
createdByName: user?.name || user?.username
});
res.json(evaluation);
} catch (error) {
console.error("Error rejecting evaluation:", error);
res.status(500).json({ error: "Failed to reject evaluation" });
}
});
// ========== SERVICE ORDERS ==========
router.get("/service-orders", async (req: Request, res: Response) => {
try {
const { status, storeId } = req.query;
let query = db.select().from(serviceOrders);
const conditions = [];
if (status) conditions.push(eq(serviceOrders.status, status as string));
if (storeId) conditions.push(eq(serviceOrders.storeId, parseInt(storeId as string)));
const orders = conditions.length > 0
? await query.where(and(...conditions)).orderBy(desc(serviceOrders.createdAt))
: await query.orderBy(desc(serviceOrders.createdAt));
res.json(orders);
} catch (error) {
console.error("Error fetching service orders:", error);
res.status(500).json({ error: "Failed to fetch service orders" });
}
});
router.get("/service-orders/:id", async (req: Request, res: Response) => {
try {
const [order] = await db.select().from(serviceOrders)
.where(eq(serviceOrders.id, parseInt(req.params.id)));
if (!order) return res.status(404).json({ error: "Service order not found" });
const items = await db.select().from(serviceOrderItems)
.where(eq(serviceOrderItems.serviceOrderId, order.id));
res.json({ ...order, items });
} catch (error) {
console.error("Error fetching service order:", error);
res.status(500).json({ error: "Failed to fetch service order" });
}
});
router.post("/service-orders", async (req: Request, res: Response) => {
try {
const orderNumber = `SO-${new Date().getFullYear()}-${String(Date.now()).slice(-6)}`;
const [order] = await db.insert(serviceOrders)
.values({ ...req.body, orderNumber })
.returning();
if (req.body.deviceId) {
await db.update(mobileDevices)
.set({ status: "in_service", lastServiceDate: sql`CURRENT_DATE` })
.where(eq(mobileDevices.id, req.body.deviceId));
}
// Log activity with user context
const user = (req as any).user;
await logActivity({
activityType: "service_order",
entityType: "service_order",
entityId: order.id,
title: `Nova O.S. criada: ${orderNumber}`,
description: `${req.body.brand || ''} ${req.body.model || ''} - ${req.body.customerName || 'Cliente'} - ${req.body.serviceType || 'Serviço'}`,
severity: "info",
storeId: req.body.storeId,
metadata: { orderNumber, serviceType: req.body.serviceType },
tenantId: user?.tenantId,
createdBy: user?.id,
createdByName: user?.name || user?.username
});
res.json(order);
} catch (error) {
console.error("Error creating service order:", error);
res.status(500).json({ error: "Failed to create service order" });
}
});
router.put("/service-orders/:id", async (req: Request, res: Response) => {
try {
const partsCost = String(parseFloat(req.body.partsCost || 0));
const laborCost = String(parseFloat(req.body.laborCost || 0));
const totalCost = String(parseFloat(partsCost) + parseFloat(laborCost));
const updatePayload: any = { ...req.body, partsCost, laborCost, totalCost, updatedAt: sql`CURRENT_TIMESTAMP` };
if (req.body.checklistCompletedBy) {
updatePayload.checklistCompletedAt = sql`CURRENT_TIMESTAMP`;
}
const [order] = await db.update(serviceOrders)
.set(updatePayload)
.where(eq(serviceOrders.id, parseInt(req.params.id)))
.returning();
// Auto-deduct parts from stock when OS is completed (RN-02)
if (req.body.status === "completed" || req.body.status === "ready_pickup") {
const items = await db.select().from(serviceOrderItems)
.where(and(
eq(serviceOrderItems.serviceOrderId, order.id),
eq(serviceOrderItems.itemType, "part"),
eq(serviceOrderItems.status, "pending")
));
for (const item of items) {
if (item.itemCode) {
const [product] = await db.select().from(products)
.where(eq(products.code, item.itemCode));
if (product) {
const newQty = Math.max(0, parseFloat(product.stockQty as string || "0") - (item.quantity || 1));
await db.update(products)
.set({ stockQty: String(newQty) })
.where(eq(products.id, product.id));
}
}
await db.update(serviceOrderItems)
.set({ status: "applied" })
.where(eq(serviceOrderItems.id, item.id));
}
// Auto-create warranty when OS is completed
if (req.body.status === "completed" && order.imei) {
const warrantyDays = order.serviceType === "repair" ? 90 : order.serviceType === "maintenance" ? 30 : 60;
const startDate = new Date().toISOString().split('T')[0];
const endDateObj = new Date();
endDateObj.setDate(endDateObj.getDate() + warrantyDays);
await db.insert(serviceWarranties).values({
tenantId: order.tenantId,
storeId: order.storeId,
serviceOrderId: order.id,
deviceId: order.deviceId,
imei: order.imei,
serviceType: order.serviceType || "repair",
warrantyDays,
startDate,
endDate: endDateObj.toISOString().split('T')[0],
customerName: order.customerName,
customerPhone: order.customerPhone,
description: `Garantia automática - OS ${order.orderNumber}`
});
}
}
// Se a OS é de Trade-In e tem uma avaliação vinculada, atualizar o status da avaliação também
if (order.isInternal && order.sourceEvaluationId && req.body.evaluationStatus) {
const evaluationStatusMap: Record<string, string> = {
"pending": "pending",
"in_analysis": "pending", // Em análise ainda é pendente na avaliação
"approved": "approved",
"rejected": "rejected"
};
const newEvalStatus = evaluationStatusMap[req.body.evaluationStatus];
if (newEvalStatus) {
await db.update(deviceEvaluations)
.set({
status: newEvalStatus,
estimatedValue: req.body.evaluatedValue || req.body.estimatedValue,
updatedAt: sql`CURRENT_TIMESTAMP`
})
.where(eq(deviceEvaluations.id, order.sourceEvaluationId));
}
}
res.json(order);
} catch (error) {
console.error("Error updating service order:", error);
res.status(500).json({ error: "Failed to update service order" });
}
});
// Concluir O.S. de preparação interna - liberar dispositivo para estoque
router.put("/service-orders/:id/complete-preparation", async (req: Request, res: Response) => {
try {
const { sellingPrice, notes } = req.body;
// Validar sellingPrice se fornecido
if (sellingPrice !== undefined && sellingPrice !== null && sellingPrice !== '') {
const price = parseFloat(sellingPrice);
if (isNaN(price) || price < 0) {
return res.status(400).json({ error: "Preço de venda inválido" });
}
}
const [order] = await db.select().from(serviceOrders)
.where(eq(serviceOrders.id, parseInt(req.params.id)));
if (!order) return res.status(404).json({ error: "Service order not found" });
if (!order.isInternal) return res.status(400).json({ error: "Esta O.S. não é interna" });
// Atualizar O.S. como concluída
const [updatedOrder] = await db.update(serviceOrders)
.set({
status: "completed",
actualCompletionDate: sql`CURRENT_DATE`,
diagnosisNotes: notes || order.diagnosisNotes,
updatedAt: sql`CURRENT_TIMESTAMP`
})
.where(eq(serviceOrders.id, parseInt(req.params.id)))
.returning();
// Liberar dispositivo para estoque
if (order.deviceId) {
const updateData: any = {
status: "in_stock",
notes: notes || undefined,
updatedAt: sql`CURRENT_TIMESTAMP`
};
if (sellingPrice) {
updateData.sellingPrice = sellingPrice;
}
await db.update(mobileDevices)
.set(updateData)
.where(eq(mobileDevices.id, order.deviceId));
// Registrar histórico
const [device] = await db.select().from(mobileDevices)
.where(eq(mobileDevices.id, order.deviceId));
if (device) {
await db.insert(deviceHistory).values({
deviceId: device.id,
imei: device.imei,
eventType: "preparation_complete",
fromLocation: "Preparação Trade-In",
toLocation: `Estoque - Loja ${order.storeId || 1}`,
referenceType: "service_order",
referenceId: order.id,
notes: `Preparação concluída. Dispositivo liberado para venda.`,
createdBy: (req as any).user?.id
});
}
}
res.json({ order: updatedOrder, message: "Preparação concluída. Dispositivo liberado para estoque." });
} catch (error) {
console.error("Error completing preparation:", error);
res.status(500).json({ error: "Failed to complete preparation" });
}
});
router.post("/service-orders/:id/items", async (req: Request, res: Response) => {
try {
const quantity = parseInt(req.body.quantity || 1);
const unitPrice = parseFloat(req.body.unitPrice);
const totalPrice = quantity * unitPrice;
const [item] = await db.insert(serviceOrderItems).values({
...req.body,
serviceOrderId: parseInt(req.params.id),
quantity,
unitPrice,
totalPrice
}).returning();
const items = await db.select().from(serviceOrderItems)
.where(eq(serviceOrderItems.serviceOrderId, parseInt(req.params.id)));
const partsCost = items.filter(i => i.itemType === 'part').reduce((sum, i) => sum + parseFloat(i.totalPrice as any), 0);
const laborCost = items.filter(i => i.itemType === 'labor').reduce((sum, i) => sum + parseFloat(i.totalPrice as any), 0);
await db.update(serviceOrders)
.set({ partsCost: String(partsCost), laborCost: String(laborCost), totalCost: String(partsCost + laborCost), updatedAt: sql`CURRENT_TIMESTAMP` })
.where(eq(serviceOrders.id, parseInt(req.params.id)));
res.json(item);
} catch (error) {
console.error("Error adding service order item:", error);
res.status(500).json({ error: "Failed to add item" });
}
});
router.get("/service-orders/:id/items", async (req: Request, res: Response) => {
try {
const items = await db.select().from(serviceOrderItems)
.where(eq(serviceOrderItems.serviceOrderId, parseInt(req.params.id)))
.orderBy(desc(serviceOrderItems.createdAt));
res.json(items);
} catch (error) {
console.error("Error fetching service order items:", error);
res.status(500).json({ error: "Failed to fetch items" });
}
});
router.delete("/service-orders/:id/items/:itemId", async (req: Request, res: Response) => {
try {
const serviceOrderId = parseInt(req.params.id);
const itemId = parseInt(req.params.itemId);
await db.delete(serviceOrderItems).where(
and(
eq(serviceOrderItems.id, itemId),
eq(serviceOrderItems.serviceOrderId, serviceOrderId)
)
);
const items = await db.select().from(serviceOrderItems)
.where(eq(serviceOrderItems.serviceOrderId, serviceOrderId));
const partsCost = items.filter(i => i.itemType === 'part').reduce((sum, i) => sum + parseFloat(i.totalPrice as any), 0);
const laborCost = items.filter(i => i.itemType === 'labor').reduce((sum, i) => sum + parseFloat(i.totalPrice as any), 0);
await db.update(serviceOrders)
.set({ partsCost: String(partsCost), laborCost: String(laborCost), totalCost: String(partsCost + laborCost), updatedAt: sql`CURRENT_TIMESTAMP` })
.where(eq(serviceOrders.id, serviceOrderId));
res.json({ success: true, items });
} catch (error) {
console.error("Error removing service order item:", error);
res.status(500).json({ error: "Failed to remove item" });
}
});
// ========== POS SESSIONS ==========
router.get("/pos-sessions", async (req: Request, res: Response) => {
try {
const { storeId, status } = req.query;
let query = db.select().from(posSessions);
const conditions = [];
if (storeId) conditions.push(eq(posSessions.storeId, parseInt(storeId as string)));
if (status) conditions.push(eq(posSessions.status, status as string));
const sessions = conditions.length > 0
? await query.where(and(...conditions)).orderBy(desc(posSessions.createdAt))
: await query.orderBy(desc(posSessions.createdAt));
res.json(sessions);
} catch (error) {
console.error("Error fetching POS sessions:", error);
res.status(500).json({ error: "Failed to fetch sessions" });
}
});
router.post("/pos-sessions/open", async (req: Request, res: Response) => {
try {
const existingOpen = await db.select().from(posSessions)
.where(and(
eq(posSessions.storeId, req.body.storeId),
eq(posSessions.cashierId, req.body.cashierId),
eq(posSessions.status, "open")
));
if (existingOpen.length > 0) {
return res.json(existingOpen[0]);
}
const [session] = await db.insert(posSessions).values({
...req.body,
cashierName: (req as any).user?.name || req.body.cashierName
}).returning();
res.json(session);
} catch (error) {
console.error("Error opening POS session:", error);
res.status(500).json({ error: "Failed to open session" });
}
});
router.put("/pos-sessions/:id/close", async (req: Request, res: Response) => {
try {
const sales = await db.select().from(posSales)
.where(eq(posSales.sessionId, parseInt(req.params.id)));
const totalSales = sales.filter(s => s.status === 'completed')
.reduce((sum, s) => sum + parseFloat(s.totalAmount as any), 0);
const totalRefunds = sales.filter(s => s.status === 'refunded')
.reduce((sum, s) => sum + parseFloat(s.totalAmount as any), 0);
const [session] = await db.update(posSessions)
.set({
sessionEndTime: sql`CURRENT_TIMESTAMP`,
closingBalance: req.body.closingBalance,
totalSales: String(totalSales),
totalRefunds: String(totalRefunds),
netSales: String(totalSales - totalRefunds),
transactionCount: sales.length,
status: "closed"
})
.where(eq(posSessions.id, parseInt(req.params.id)))
.returning();
res.json(session);
} catch (error) {
console.error("Error closing POS session:", error);
res.status(500).json({ error: "Failed to close session" });
}
});
// ========== CASH MOVEMENTS (Sangria/Reforço) ==========
router.get("/cash-movements", async (req: Request, res: Response) => {
try {
const { sessionId } = req.query;
const tenantId = (req.user as any)?.tenantId;
const conditions = [];
if (sessionId) conditions.push(eq(posCashMovements.sessionId, parseInt(sessionId as string)));
if (tenantId) conditions.push(eq(posCashMovements.tenantId, tenantId));
const movements = conditions.length > 0
? await db.select().from(posCashMovements).where(and(...conditions)).orderBy(desc(posCashMovements.createdAt))
: await db.select().from(posCashMovements).orderBy(desc(posCashMovements.createdAt));
res.json(movements);
} catch (error) {
console.error("Error fetching cash movements:", error);
res.status(500).json({ error: "Failed to fetch cash movements" });
}
});
router.post("/cash-movements", async (req: Request, res: Response) => {
try {
const user = (req as any).user;
const [movement] = await db.insert(posCashMovements).values({
...req.body,
performedBy: user?.id,
performedByName: user?.name || user?.username,
tenantId: user?.tenantId
}).returning();
res.json(movement);
} catch (error) {
console.error("Error creating cash movement:", error);
res.status(500).json({ error: "Failed to create cash movement" });
}
});
// ========== SERVICE WARRANTIES ==========
router.get("/warranties", async (req: Request, res: Response) => {
try {
const { imei, status, serviceOrderId } = req.query;
const tenantId = (req.user as any)?.tenantId;
const conditions = [];
if (tenantId) conditions.push(eq(serviceWarranties.tenantId, tenantId));
if (imei) conditions.push(eq(serviceWarranties.imei, imei as string));
if (status) conditions.push(eq(serviceWarranties.status, status as string));
if (serviceOrderId) conditions.push(eq(serviceWarranties.serviceOrderId, parseInt(serviceOrderId as string)));
const warranties = conditions.length > 0
? await db.select().from(serviceWarranties).where(and(...conditions)).orderBy(desc(serviceWarranties.createdAt))
: await db.select().from(serviceWarranties).orderBy(desc(serviceWarranties.createdAt));
res.json(warranties);
} catch (error) {
console.error("Error fetching warranties:", error);
res.status(500).json({ error: "Failed to fetch warranties" });
}
});
router.post("/warranties", async (req: Request, res: Response) => {
try {
const user = (req as any).user;
const startDate = req.body.startDate || new Date().toISOString().split('T')[0];
const warrantyDays = parseInt(req.body.warrantyDays || 90);
const endDateObj = new Date(startDate);
endDateObj.setDate(endDateObj.getDate() + warrantyDays);
const endDate = endDateObj.toISOString().split('T')[0];
const [warranty] = await db.insert(serviceWarranties).values({
...req.body,
startDate,
endDate,
warrantyDays,
tenantId: user?.tenantId
}).returning();
res.json(warranty);
} catch (error) {
console.error("Error creating warranty:", error);
res.status(500).json({ error: "Failed to create warranty" });
}
});
router.get("/warranties/check/:imei", async (req: Request, res: Response) => {
try {
const tenantId = (req.user as any)?.tenantId;
const today = new Date().toISOString().split('T')[0];
const conditions = [
eq(serviceWarranties.imei, req.params.imei),
eq(serviceWarranties.status, "active"),
gte(serviceWarranties.endDate, today)
];
if (tenantId) conditions.push(eq(serviceWarranties.tenantId, tenantId));
const warranties = await db.select().from(serviceWarranties)
.where(and(...conditions))
.orderBy(desc(serviceWarranties.endDate));
res.json({ hasActiveWarranty: warranties.length > 0, warranties });
} catch (error) {
console.error("Error checking warranty:", error);
res.status(500).json({ error: "Failed to check warranty" });
}
});
router.put("/warranties/:id/claim", async (req: Request, res: Response) => {
try {
const [warranty] = await db.update(serviceWarranties)
.set({ status: "claimed", claimedAt: sql`CURRENT_TIMESTAMP`, claimNotes: req.body.notes })
.where(eq(serviceWarranties.id, parseInt(req.params.id)))
.returning();
res.json(warranty);
} catch (error) {
console.error("Error claiming warranty:", error);
res.status(500).json({ error: "Failed to claim warranty" });
}
});
// ========== STOCK ALERTS ==========
router.get("/stock-alerts", async (req: Request, res: Response) => {
try {
const tenantId = (req.user as any)?.tenantId;
const conditions = [
eq(products.status, "active"),
sql`CAST(${products.stockQty} AS NUMERIC) <= CAST(${products.minStock} AS NUMERIC)`,
sql`CAST(${products.minStock} AS NUMERIC) > 0`
];
if (tenantId) conditions.push(eq(products.tenantId, tenantId));
const lowStockProducts = await db.select().from(products)
.where(and(...conditions))
.orderBy(asc(products.name));
res.json(lowStockProducts);
} catch (error) {
console.error("Error fetching stock alerts:", error);
res.status(500).json({ error: "Failed to fetch stock alerts" });
}
});
// ========== REPORTS ==========
router.get("/reports/os-by-status", async (req: Request, res: Response) => {
try {
const tenantId = (req.user as any)?.tenantId;
if (!tenantId) return res.status(403).json({ error: "Tenant not identified" });
const result = await db.execute(sql`
SELECT status, COUNT(*) as count,
COALESCE(SUM(CAST(total_cost AS NUMERIC)), 0) as total_value
FROM service_orders WHERE tenant_id = ${tenantId}
GROUP BY status ORDER BY count DESC
`);
res.json(result.rows || result);
} catch (error) {
console.error("Error fetching OS report:", error);
res.status(500).json({ error: "Failed to fetch report" });
}
});
router.get("/reports/os-by-technician", async (req: Request, res: Response) => {
try {
const tenantId = (req.user as any)?.tenantId;
if (!tenantId) return res.status(403).json({ error: "Tenant not identified" });
const result = await db.execute(sql`
SELECT technician_name,
COUNT(*) as total_os,
COUNT(*) FILTER (WHERE status = 'completed') as completed,
COUNT(*) FILTER (WHERE status IN ('open','diagnosis','in_repair')) as in_progress,
COALESCE(SUM(CAST(total_cost AS NUMERIC)) FILTER (WHERE status = 'completed'), 0) as total_revenue
FROM service_orders
WHERE technician_name IS NOT NULL AND tenant_id = ${tenantId}
GROUP BY technician_name ORDER BY total_os DESC
`);
res.json(result.rows || result);
} catch (error) {
console.error("Error fetching technician report:", error);
res.status(500).json({ error: "Failed to fetch report" });
}
});
router.get("/reports/sales-by-seller", async (req: Request, res: Response) => {
try {
const { dateFrom, dateTo } = req.query;
const tenantId = (req.user as any)?.tenantId;
if (!tenantId) return res.status(403).json({ error: "Tenant not identified" });
let dateFilter = sql`TRUE`;
if (dateFrom && dateTo) {
dateFilter = sql`created_at >= ${dateFrom}::date AND created_at <= (${dateTo}::date + INTERVAL '1 day')`;
} else {
dateFilter = sql`DATE(created_at) >= CURRENT_DATE - INTERVAL '30 days'`;
}
const tenantFilter = sql`AND tenant_id = ${tenantId}`;
const result = await db.execute(sql`
SELECT sold_by,
COUNT(*) as total_sales,
COALESCE(SUM(CAST(total_amount AS NUMERIC)), 0) as total_revenue,
COALESCE(AVG(CAST(total_amount AS NUMERIC)), 0) as avg_ticket,
COUNT(DISTINCT DATE(created_at)) as active_days
FROM pos_sales
WHERE status = 'completed' AND ${dateFilter} ${tenantFilter}
GROUP BY sold_by ORDER BY total_revenue DESC
`);
res.json(result.rows || result);
} catch (error) {
console.error("Error fetching sales report:", error);
res.status(500).json({ error: "Failed to fetch report" });
}
});
router.get("/reports/margin-by-imei", async (req: Request, res: Response) => {
try {
const result = await db.execute(sql`
SELECT d.id, d.brand, d.model, d.imei, d.condition,
CAST(d.purchase_price AS NUMERIC) as cost,
CAST(d.selling_price AS NUMERIC) as sale_price,
CAST(d.selling_price AS NUMERIC) - CAST(d.purchase_price AS NUMERIC) as margin,
CASE WHEN CAST(d.purchase_price AS NUMERIC) > 0
THEN ROUND(((CAST(d.selling_price AS NUMERIC) - CAST(d.purchase_price AS NUMERIC)) / CAST(d.purchase_price AS NUMERIC)) * 100, 2)
ELSE 0 END as margin_percent,
d.status
FROM mobile_devices d
WHERE d.purchase_price IS NOT NULL AND CAST(d.purchase_price AS NUMERIC) > 0
ORDER BY margin DESC
`);
res.json(result.rows || result);
} catch (error) {
console.error("Error fetching margin report:", error);
res.status(500).json({ error: "Failed to fetch report" });
}
});
router.get("/reports/daily-cash", async (req: Request, res: Response) => {
try {
const { date } = req.query;
const tenantId = (req.user as any)?.tenantId;
if (!tenantId) return res.status(403).json({ error: "Tenant not identified" });
const targetDate = date ? String(date) : new Date().toISOString().split('T')[0];
const tenantFilter = sql`AND tenant_id = ${tenantId}`;
const summaryResult = await db.execute(sql`
SELECT
COALESCE(SUM(CAST(total_amount AS NUMERIC)), 0) as total_sales,
COUNT(*) as sale_count,
COALESCE(SUM(CAST(total_amount AS NUMERIC)) FILTER (WHERE payment_method = 'cash'), 0) as cash_total,
COALESCE(SUM(CAST(total_amount AS NUMERIC)) FILTER (WHERE payment_method IN ('credit','debit')), 0) as card_total,
COALESCE(SUM(CAST(total_amount AS NUMERIC)) FILTER (WHERE payment_method = 'pix'), 0) as pix_total,
COALESCE(SUM(CAST(total_amount AS NUMERIC)) FILTER (WHERE payment_method = 'combined'), 0) as combined_total
FROM pos_sales
WHERE status = 'completed' AND DATE(created_at) = ${targetDate}::date ${tenantFilter}
`);
const movementsResult = await db.execute(sql`
SELECT
COALESCE(SUM(CAST(amount AS NUMERIC)) FILTER (WHERE type = 'withdrawal'), 0) as withdrawals,
COALESCE(SUM(CAST(amount AS NUMERIC)) FILTER (WHERE type = 'reinforcement'), 0) as reinforcements
FROM pos_cash_movements
WHERE DATE(created_at) = ${targetDate}::date ${tenantFilter}
`);
const salesResult = await db.execute(sql`
SELECT id, sale_number, customer_name, total_amount, payment_method, sold_by,
discount_amount, subtotal, created_at, notes
FROM pos_sales
WHERE status = 'completed' AND DATE(created_at) = ${targetDate}::date ${tenantFilter}
ORDER BY created_at DESC
`);
const bySellerResult = await db.execute(sql`
SELECT
sold_by,
COUNT(*) as sale_count,
COALESCE(SUM(CAST(total_amount AS NUMERIC)), 0) as total,
COALESCE(SUM(CAST(total_amount AS NUMERIC)) FILTER (WHERE payment_method = 'cash'), 0) as cash_total,
COALESCE(SUM(CAST(total_amount AS NUMERIC)) FILTER (WHERE payment_method IN ('credit','debit')), 0) as card_total,
COALESCE(SUM(CAST(total_amount AS NUMERIC)) FILTER (WHERE payment_method = 'credit'), 0) as credit_total,
COALESCE(SUM(CAST(total_amount AS NUMERIC)) FILTER (WHERE payment_method = 'debit'), 0) as debit_total,
COALESCE(SUM(CAST(total_amount AS NUMERIC)) FILTER (WHERE payment_method = 'pix'), 0) as pix_total,
COALESCE(SUM(CAST(total_amount AS NUMERIC)) FILTER (WHERE payment_method = 'combined'), 0) as combined_total,
COALESCE(SUM(CAST(discount_amount AS NUMERIC)), 0) as total_discount
FROM pos_sales
WHERE status = 'completed' AND DATE(created_at) = ${targetDate}::date ${tenantFilter}
GROUP BY sold_by
ORDER BY total DESC
`);
const summary = (summaryResult.rows as any[])?.[0] || {};
const movements = (movementsResult.rows as any[])?.[0] || {};
res.json({
...summary,
withdrawals: movements.withdrawals || 0,
reinforcements: movements.reinforcements || 0,
sales: salesResult.rows || [],
bySeller: bySellerResult.rows || [],
date: targetDate
});
} catch (error) {
console.error("Error fetching daily cash report:", error);
res.status(500).json({ error: "Failed to fetch report" });
}
});
router.get("/reports/stock-turnover", async (req: Request, res: Response) => {
try {
const result = await db.execute(sql`
SELECT p.id, p.code, p.name, p.category,
CAST(p.stock_qty AS NUMERIC) as current_stock,
CAST(p.min_stock AS NUMERIC) as min_stock,
COALESCE((SELECT COUNT(*) FROM pos_sale_items psi WHERE psi.item_code = p.code AND psi.created_at >= CURRENT_DATE - INTERVAL '30 days'), 0) as sales_30d,
CASE WHEN CAST(p.stock_qty AS NUMERIC) > 0
THEN ROUND(COALESCE((SELECT COUNT(*) FROM pos_sale_items psi WHERE psi.item_code = p.code AND psi.created_at >= CURRENT_DATE - INTERVAL '30 days'), 0)::numeric / CAST(p.stock_qty AS NUMERIC), 2)
ELSE 0 END as turnover_ratio
FROM products p
WHERE p.status = 'active'
ORDER BY turnover_ratio DESC
LIMIT 50
`);
res.json(result.rows || result);
} catch (error) {
console.error("Error fetching stock turnover:", error);
res.status(500).json({ error: "Failed to fetch report" });
}
});
// ========== POS SALES ==========
router.get("/sales", async (req: Request, res: Response) => {
try {
const { sessionId, storeId, detailed, limit: limitParam, dateFrom, dateTo, soldBy } = req.query;
let query = db.select().from(posSales);
const conditions = [];
if (sessionId) conditions.push(eq(posSales.sessionId, parseInt(sessionId as string)));
if (storeId) conditions.push(eq(posSales.storeId, parseInt(storeId as string)));
if (soldBy) conditions.push(eq(posSales.soldBy, soldBy as string));
if (dateFrom) conditions.push(gte(posSales.createdAt, new Date(dateFrom as string)));
if (dateTo) {
const endDate = new Date(dateTo as string);
endDate.setHours(23, 59, 59, 999);
conditions.push(lte(posSales.createdAt, endDate));
}
let sales = conditions.length > 0
? await query.where(and(...conditions)).orderBy(desc(posSales.createdAt)).limit(parseInt(limitParam as string) || 100)
: await query.orderBy(desc(posSales.createdAt)).limit(parseInt(limitParam as string) || 100);
// Se detailed=true, incluir contagem de itens e formas de pagamento
if (detailed === "true") {
const salesWithDetails = await Promise.all(sales.map(async (sale) => {
// Buscar itens da venda
const items = await db.select().from(posSaleItems).where(eq(posSaleItems.saleId, sale.id));
const itemCount = items.reduce((sum, item) => sum + (item.quantity || 1), 0);
// Parse payment details para extrair métodos de pagamento
const paymentMethods = sale.paymentDetails || [];
// Buscar crédito usado (se existir no paymentDetails ou tradeInValue)
const creditUsed = parseFloat(sale.tradeInValue || "0") + (
Array.isArray(sale.paymentDetails)
? sale.paymentDetails.reduce((sum: number, pd: any) => pd.method === "credit" ? sum + parseFloat(pd.amount || "0") : sum, 0)
: 0
);
return {
...sale,
itemCount,
paymentMethods: Array.isArray(paymentMethods) ? paymentMethods : [],
creditUsed: creditUsed.toString(),
sellerId: sale.soldBy ? parseInt(sale.soldBy) : null
};
}));
return res.json({ sales: salesWithDetails });
}
res.json(sales);
} catch (error) {
console.error("Error fetching sales:", error);
res.status(500).json({ error: "Failed to fetch sales" });
}
});
router.post("/sales", async (req: Request, res: Response) => {
try {
console.log("[SALES] Recebendo requisição de venda:", JSON.stringify(req.body, null, 2));
const saleNumber = `VD-${new Date().getFullYear()}-${String(Date.now()).slice(-6)}`;
const { items, creditUsed, useCredits, personId, paymentMethods, subtotal, ...saleData } = req.body;
// Validação server-side: verificar totais
const subtotalAmount = parseFloat(subtotal || "0");
const totalAmount = parseFloat(saleData.totalAmount || "0");
const creditUsedAmount = parseFloat(creditUsed || "0");
const discountAmount = parseFloat(saleData.discountAmount || "0");
const tradeInValue = parseFloat(saleData.tradeInValue || "0");
if (totalAmount < 0) {
return res.status(400).json({ error: "Total da venda não pode ser negativo" });
}
// Calcular baseTotal dos itens
const itemsTotal = items?.reduce((sum: number, item: any) => {
return sum + (parseFloat(item.unitPrice || "0") * (item.quantity || 1));
}, 0) || 0;
const baseTotal = itemsTotal - discountAmount;
// Validar: créditos não podem exceder o valor base da venda
const totalCreditsUsed = tradeInValue + creditUsedAmount;
if (totalCreditsUsed > baseTotal) {
return res.status(400).json({ error: "Créditos excedem o valor da compra" });
}
// Validar consistência: totalAmount deve ser baseTotal - tradeInValue - creditUsed
const expectedTotal = Math.max(0, baseTotal - totalCreditsUsed);
if (Math.abs(totalAmount - expectedTotal) > 0.01) {
console.log("[SALES] Inconsistência:", { totalAmount, expectedTotal, baseTotal, tradeInValue, creditUsedAmount });
return res.status(400).json({ error: "Inconsistência nos valores da venda" });
}
// Verificar se há créditos suficientes para o cliente (tradeIn + creditUsed)
// O tradeInValue também consome créditos da tabela customerCredits
const totalCreditToConsume = tradeInValue + creditUsedAmount;
if (totalCreditToConsume > 0 && personId) {
const personCredits = await db.select().from(customerCredits)
.where(and(
eq(customerCredits.personId, parseInt(personId)),
eq(customerCredits.status, "active")
));
const availableCredit = personCredits.reduce((sum, c) => sum + parseFloat(c.remainingAmount || "0"), 0);
if (totalCreditToConsume > availableCredit) {
return res.status(400).json({
error: `Crédito insuficiente. Disponível: R$ ${availableCredit.toFixed(2)}, Solicitado: R$ ${totalCreditToConsume.toFixed(2)}`
});
}
}
// Executar tudo em uma transação para garantir atomicidade
const result = await db.transaction(async (tx) => {
const [sale] = await tx.insert(posSales)
.values({
...saleData,
subtotal: String(subtotalAmount),
saleNumber,
soldBy: (req as any).user?.id
})
.returning();
// Processar uso de créditos atomicamente com a venda
// Inclui tanto tradeInValue quanto creditUsed para evitar uso duplo
const totalCreditToConsume = tradeInValue + creditUsedAmount;
if (totalCreditToConsume > 0 && personId) {
const personCredits = await tx.select().from(customerCredits)
.where(and(
eq(customerCredits.personId, parseInt(personId)),
eq(customerCredits.status, "active")
))
.orderBy(asc(customerCredits.createdAt)); // FIFO
let remainingToUse = totalCreditToConsume;
for (const credit of personCredits) {
if (remainingToUse <= 0) break;
const creditRemaining = parseFloat(credit.remainingAmount || "0");
const toUse = Math.min(remainingToUse, creditRemaining);
if (toUse > 0) {
const newRemaining = creditRemaining - toUse;
const newUsed = parseFloat(credit.usedAmount || "0") + toUse;
await tx.update(customerCredits)
.set({
usedAmount: newUsed.toFixed(2),
remainingAmount: newRemaining.toFixed(2),
status: newRemaining <= 0 ? "used" : "active",
usedInSaleId: sale.id,
updatedAt: new Date()
})
.where(eq(customerCredits.id, credit.id));
remainingToUse -= toUse;
}
}
}
if (items && items.length > 0) {
for (const item of items) {
await tx.insert(posSaleItems).values({
...item,
saleId: sale.id
});
if (item.deviceId) {
await tx.update(mobileDevices)
.set({
status: "sold",
soldDate: sql`CURRENT_DATE`,
soldToCustomer: saleData.customerId
})
.where(eq(mobileDevices.id, item.deviceId));
await tx.insert(deviceHistory).values({
deviceId: item.deviceId,
imei: item.imei,
eventType: "sold",
fromLocation: `Store ${saleData.storeId}`,
toLocation: saleData.customerName || "Customer",
referenceType: "sale",
referenceId: sale.id,
createdBy: (req as any).user?.id
});
}
}
}
// Só atualiza sessão se existir
if (sale.sessionId) {
await tx.update(posSessions)
.set({
transactionCount: sql`transaction_count + 1`,
totalSales: sql`total_sales + ${sale.totalAmount}`
})
.where(eq(posSessions.id, sale.sessionId));
}
// Gerar contas a receber por método de pagamento
const paymentMethodLabels: Record<string, string> = {
cash: "Dinheiro",
credit_card: "Cartão de Crédito",
debit_card: "Cartão de Débito",
pix: "PIX",
customer_credit: "Crédito Cliente"
};
if (paymentMethods && paymentMethods.length > 0) {
for (const pm of paymentMethods) {
if (pm.amount > 0) {
const docNumber = `${sale.saleNumber}-${pm.method.toUpperCase()}`;
const dueDate = pm.method === "credit_card" ?
new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) : // 30 dias para crédito
new Date(); // Imediato para outros
await tx.insert(finAccountsReceivable).values({
tenantId: (req as any).user?.tenantId || 1,
documentNumber: docNumber,
customerId: personId ? parseInt(personId) : null,
customerName: saleData.customerName || "Cliente PDV",
description: `Venda PDV ${sale.saleNumber} - ${paymentMethodLabels[pm.method] || pm.method}`,
issueDate: sql`CURRENT_DATE`,
dueDate: sql`${dueDate.toISOString().split('T')[0]}::date`,
originalAmount: String(pm.amount),
discountAmount: "0",
interestAmount: "0",
fineAmount: "0",
receivedAmount: pm.method === "cash" || pm.method === "pix" || pm.method === "debit_card" ? String(pm.amount) : "0",
remainingAmount: pm.method === "credit_card" ? String(pm.amount) : "0",
status: pm.method === "credit_card" ? "pending" : "received",
receivedAt: pm.method !== "credit_card" ? sql`NOW()` : null,
salesOrderId: sale.id,
notes: `Origem: PDV - Método: ${paymentMethodLabels[pm.method] || pm.method}`
});
}
}
}
return sale;
});
res.json(result);
} catch (error) {
console.error("Error creating sale:", error);
res.status(500).json({ error: "Failed to create sale" });
}
});
// ========== STOCK TRANSFERS ==========
router.get("/transfers", async (req: Request, res: Response) => {
try {
const transfers = await db.select().from(stockTransfers)
.orderBy(desc(stockTransfers.createdAt));
res.json(transfers);
} catch (error) {
console.error("Error fetching transfers:", error);
res.status(500).json({ error: "Failed to fetch transfers" });
}
});
router.post("/transfers", async (req: Request, res: Response) => {
try {
const transferNumber = `TR-${new Date().getFullYear()}-${String(Date.now()).slice(-6)}`;
const { items, ...transferData } = req.body;
const [transfer] = await db.insert(stockTransfers)
.values({ ...transferData, transferNumber, totalItems: items?.length || 0 })
.returning();
if (items && items.length > 0) {
for (const item of items) {
await db.insert(stockTransferItems).values({
transferId: transfer.id,
deviceId: item.deviceId,
imei: item.imei
});
}
}
res.json(transfer);
} catch (error) {
console.error("Error creating transfer:", error);
res.status(500).json({ error: "Failed to create transfer" });
}
});
router.put("/transfers/:id/approve", async (req: Request, res: Response) => {
try {
const [transfer] = await db.update(stockTransfers)
.set({ status: "approved", approvedBy: (req as any).user?.id, updatedAt: new Date() })
.where(eq(stockTransfers.id, parseInt(req.params.id)))
.returning();
res.json(transfer);
} catch (error) {
console.error("Error approving transfer:", error);
res.status(500).json({ error: "Failed to approve transfer" });
}
});
router.put("/transfers/:id/receive", async (req: Request, res: Response) => {
try {
const [transfer] = await db.select().from(stockTransfers)
.where(eq(stockTransfers.id, parseInt(req.params.id)));
const items = await db.select().from(stockTransferItems)
.where(eq(stockTransferItems.transferId, parseInt(req.params.id)));
for (const item of items) {
await db.update(mobileDevices)
.set({
storeId: transfer.toStoreId,
warehouseId: transfer.toWarehouseId,
updatedAt: new Date()
})
.where(eq(mobileDevices.id, item.deviceId));
await db.update(stockTransferItems)
.set({ status: "received" })
.where(eq(stockTransferItems.id, item.id));
await db.insert(deviceHistory).values({
deviceId: item.deviceId,
imei: item.imei,
eventType: "transferred",
fromLocation: transfer.fromStoreId ? `Store ${transfer.fromStoreId}` : `Warehouse ${transfer.fromWarehouseId}`,
toLocation: transfer.toStoreId ? `Store ${transfer.toStoreId}` : `Warehouse ${transfer.toWarehouseId}`,
referenceType: "transfer",
referenceId: transfer.id,
createdBy: (req as any).user?.id
});
}
const [updated] = await db.update(stockTransfers)
.set({
status: "received",
receivedDate: sql`CURRENT_DATE`,
receivedBy: (req as any).user?.id,
updatedAt: sql`CURRENT_TIMESTAMP`
})
.where(eq(stockTransfers.id, parseInt(req.params.id)))
.returning();
res.json(updated);
} catch (error) {
console.error("Error receiving transfer:", error);
res.status(500).json({ error: "Failed to receive transfer" });
}
});
// ========== PDV PRODUCTS ==========
router.get("/pdv-products", async (req: Request, res: Response) => {
try {
const tenantId = (req.user as any)?.tenantId;
const conditions = [
eq(products.status, "active"),
or(
eq(products.trackingType, "none"),
isNull(products.trackingType)
)
];
if (tenantId) {
conditions.push(eq(products.tenantId, tenantId));
}
const allProducts = await db.select().from(products)
.where(and(...conditions))
.orderBy(asc(products.name));
res.json(allProducts);
} catch (error) {
console.error("Error fetching PDV products:", error);
res.status(500).json({ error: "Failed to fetch products" });
}
});
// ========== DASHBOARD STATS ==========
router.get("/dashboard/stats", async (req: Request, res: Response) => {
try {
const { storeId } = req.query;
const devicesInStock = await db.select({ count: sql<number>`count(*)` })
.from(mobileDevices)
.where(eq(mobileDevices.status, "in_stock"));
const openOrders = await db.select({ count: sql<number>`count(*)` })
.from(serviceOrders)
.where(or(eq(serviceOrders.status, "open"), eq(serviceOrders.status, "in_progress")));
const todaySales = await db.select({
total: sql<number>`COALESCE(SUM(total_amount), 0)`,
count: sql<number>`count(*)`
}).from(posSales)
.where(sql`DATE(created_at) = CURRENT_DATE`);
const pendingEvaluations = await db.select({ count: sql<number>`count(*)` })
.from(deviceEvaluations)
.where(eq(deviceEvaluations.status, "pending"));
res.json({
devicesInStock: devicesInStock[0]?.count || 0,
openServiceOrders: openOrders[0]?.count || 0,
todaySalesTotal: todaySales[0]?.total || 0,
todaySalesCount: todaySales[0]?.count || 0,
pendingEvaluations: pendingEvaluations[0]?.count || 0
});
} catch (error) {
console.error("Error fetching dashboard stats:", error);
res.status(500).json({ error: "Failed to fetch stats" });
}
});
// Helper function for trade-in value calculation
function calculateTradeInValue(evaluation: any): number {
const baseValues: Record<string, number> = {
"iPhone 15 Pro Max": 6000,
"iPhone 15 Pro": 5500,
"iPhone 15": 4500,
"iPhone 14 Pro Max": 5000,
"iPhone 14 Pro": 4500,
"iPhone 14": 3500,
"iPhone 13 Pro Max": 4000,
"iPhone 13 Pro": 3500,
"iPhone 13": 2800,
"iPhone 12": 2000,
"iPhone 11": 1500,
"Galaxy S24 Ultra": 5500,
"Galaxy S24+": 4500,
"Galaxy S24": 3800,
"Galaxy S23 Ultra": 4500,
"Galaxy S23": 3000,
"Galaxy S22": 2200,
"Galaxy S21": 1800
};
const modelKey = `${evaluation.brand} ${evaluation.model}`.replace(/\s+/g, " ");
let baseValue = baseValues[modelKey] || 1000;
const conditionMultiplier: Record<string, number> = {
excellent: 0.85,
good: 0.70,
fair: 0.50,
poor: 0.30
};
let value = baseValue * (conditionMultiplier[evaluation.overallCondition] || 0.5);
if (evaluation.screenCondition === "broken") value *= 0.5;
if (evaluation.screenCondition === "cracks") value *= 0.7;
if (evaluation.batteryHealth && evaluation.batteryHealth < 80) {
value *= (evaluation.batteryHealth / 100);
}
if (evaluation.waterDamageDetected) value *= 0.3;
if (!evaluation.chargerIncluded) value *= 0.95;
return Math.round(value * 100) / 100;
}
// ========== CHECKLIST TEMPLATES ==========
router.get("/checklist/templates", async (req: Request, res: Response) => {
try {
const templates = await db.select().from(tradeInChecklistTemplates)
.where(eq(tradeInChecklistTemplates.isActive, true))
.orderBy(desc(tradeInChecklistTemplates.createdAt));
res.json(templates);
} catch (error) {
console.error("Error fetching checklist templates:", error);
res.status(500).json({ error: "Failed to fetch templates" });
}
});
router.post("/checklist/templates", async (req: Request, res: Response) => {
try {
const [template] = await db.insert(tradeInChecklistTemplates)
.values(req.body)
.returning();
res.json(template);
} catch (error) {
console.error("Error creating checklist template:", error);
res.status(500).json({ error: "Failed to create template" });
}
});
router.get("/checklist/templates/:id", async (req: Request, res: Response) => {
try {
const [template] = await db.select().from(tradeInChecklistTemplates)
.where(eq(tradeInChecklistTemplates.id, parseInt(req.params.id)));
if (!template) return res.status(404).json({ error: "Template not found" });
const items = await db.select().from(tradeInChecklistItems)
.where(eq(tradeInChecklistItems.templateId, template.id))
.orderBy(asc(tradeInChecklistItems.displayOrder));
res.json({ ...template, items });
} catch (error) {
console.error("Error fetching template:", error);
res.status(500).json({ error: "Failed to fetch template" });
}
});
router.put("/checklist/templates/:id", async (req: Request, res: Response) => {
try {
const [template] = await db.update(tradeInChecklistTemplates)
.set({ ...req.body, updatedAt: sql`CURRENT_TIMESTAMP` })
.where(eq(tradeInChecklistTemplates.id, parseInt(req.params.id)))
.returning();
res.json(template);
} catch (error) {
console.error("Error updating template:", error);
res.status(500).json({ error: "Failed to update template" });
}
});
router.delete("/checklist/templates/:id", async (req: Request, res: Response) => {
try {
await db.update(tradeInChecklistTemplates)
.set({ isActive: false })
.where(eq(tradeInChecklistTemplates.id, parseInt(req.params.id)));
res.json({ success: true });
} catch (error) {
console.error("Error deleting template:", error);
res.status(500).json({ error: "Failed to delete template" });
}
});
// ========== CHECKLIST ITEMS ==========
router.get("/checklist/items/:templateId", async (req: Request, res: Response) => {
try {
const items = await db.select().from(tradeInChecklistItems)
.where(eq(tradeInChecklistItems.templateId, parseInt(req.params.templateId)))
.orderBy(asc(tradeInChecklistItems.displayOrder));
res.json(items);
} catch (error) {
console.error("Error fetching checklist items:", error);
res.status(500).json({ error: "Failed to fetch items" });
}
});
router.post("/checklist/items", async (req: Request, res: Response) => {
try {
const [item] = await db.insert(tradeInChecklistItems)
.values(req.body)
.returning();
res.json(item);
} catch (error) {
console.error("Error creating checklist item:", error);
res.status(500).json({ error: "Failed to create item" });
}
});
router.put("/checklist/items/:id", async (req: Request, res: Response) => {
try {
const [item] = await db.update(tradeInChecklistItems)
.set(req.body)
.where(eq(tradeInChecklistItems.id, parseInt(req.params.id)))
.returning();
res.json(item);
} catch (error) {
console.error("Error updating checklist item:", error);
res.status(500).json({ error: "Failed to update item" });
}
});
router.delete("/checklist/items/:id", async (req: Request, res: Response) => {
try {
await db.delete(tradeInChecklistItems)
.where(eq(tradeInChecklistItems.id, parseInt(req.params.id)));
res.json({ success: true });
} catch (error) {
console.error("Error deleting checklist item:", error);
res.status(500).json({ error: "Failed to delete item" });
}
});
// ========== EVALUATION RESULTS ==========
router.get("/evaluations/:id/results", async (req: Request, res: Response) => {
try {
const results = await db.select({
result: tradeInEvaluationResults,
item: tradeInChecklistItems
}).from(tradeInEvaluationResults)
.leftJoin(tradeInChecklistItems, eq(tradeInEvaluationResults.checklistItemId, tradeInChecklistItems.id))
.where(eq(tradeInEvaluationResults.evaluationId, parseInt(req.params.id)));
res.json(results);
} catch (error) {
console.error("Error fetching evaluation results:", error);
res.status(500).json({ error: "Failed to fetch results" });
}
});
router.post("/evaluations/:id/results", async (req: Request, res: Response) => {
try {
const evaluationId = parseInt(req.params.id);
const { results } = req.body;
// Deletar resultados anteriores
await db.delete(tradeInEvaluationResults)
.where(eq(tradeInEvaluationResults.evaluationId, evaluationId));
// Inserir novos resultados
for (const result of results) {
await db.insert(tradeInEvaluationResults).values({
evaluationId,
checklistItemId: result.checklistItemId,
result: result.result,
percentValue: result.percentValue,
notes: result.notes
});
}
res.json({ success: true, count: results.length });
} catch (error) {
console.error("Error saving evaluation results:", error);
res.status(500).json({ error: "Failed to save results" });
}
});
// ========== TRANSFER DOCUMENTS ==========
router.get("/transfer-documents", async (req: Request, res: Response) => {
try {
const { evaluationId, status } = req.query;
let query = db.select().from(tradeInTransferDocuments);
const conditions = [];
if (evaluationId) conditions.push(eq(tradeInTransferDocuments.evaluationId, parseInt(evaluationId as string)));
if (status) conditions.push(eq(tradeInTransferDocuments.status, status as string));
const docs = conditions.length > 0
? await query.where(and(...conditions)).orderBy(desc(tradeInTransferDocuments.createdAt))
: await query.orderBy(desc(tradeInTransferDocuments.createdAt));
res.json(docs);
} catch (error) {
console.error("Error fetching transfer documents:", error);
res.status(500).json({ error: "Failed to fetch documents" });
}
});
router.post("/transfer-documents", async (req: Request, res: Response) => {
try {
const documentNumber = `TRF-${new Date().getFullYear()}-${String(Date.now()).slice(-6)}`;
const [doc] = await db.insert(tradeInTransferDocuments)
.values({ ...req.body, documentNumber, status: "pending_signature" })
.returning();
res.json(doc);
} catch (error) {
console.error("Error creating transfer document:", error);
res.status(500).json({ error: "Failed to create document" });
}
});
router.get("/transfer-documents/:id", async (req: Request, res: Response) => {
try {
const [doc] = await db.select().from(tradeInTransferDocuments)
.where(eq(tradeInTransferDocuments.id, parseInt(req.params.id)));
if (!doc) return res.status(404).json({ error: "Document not found" });
res.json(doc);
} catch (error) {
console.error("Error fetching transfer document:", error);
res.status(500).json({ error: "Failed to fetch document" });
}
});
router.post("/transfer-documents/:id/sign", async (req: Request, res: Response) => {
try {
const { customerSignature, employeeSignature, employeeName, termsAccepted } = req.body;
const updateData: any = { updatedAt: sql`CURRENT_TIMESTAMP` };
if (customerSignature) {
updateData.customerSignature = customerSignature;
updateData.customerSignedAt = sql`CURRENT_TIMESTAMP`;
}
if (employeeSignature) {
updateData.employeeSignature = employeeSignature;
updateData.employeeName = employeeName;
updateData.employeeSignedAt = sql`CURRENT_TIMESTAMP`;
}
if (termsAccepted !== undefined) {
updateData.termsAccepted = termsAccepted;
}
// Se ambas assinaturas estiverem presentes, marcar como signed
const [doc] = await db.select().from(tradeInTransferDocuments)
.where(eq(tradeInTransferDocuments.id, parseInt(req.params.id)));
if (customerSignature && (doc?.employeeSignature || employeeSignature)) {
updateData.status = "signed";
} else if (employeeSignature && doc?.customerSignature) {
updateData.status = "signed";
}
const [updated] = await db.update(tradeInTransferDocuments)
.set(updateData)
.where(eq(tradeInTransferDocuments.id, parseInt(req.params.id)))
.returning();
res.json(updated);
} catch (error) {
console.error("Error signing transfer document:", error);
res.status(500).json({ error: "Failed to sign document" });
}
});
router.post("/transfer-documents/:id/complete", async (req: Request, res: Response) => {
try {
const [doc] = await db.select().from(tradeInTransferDocuments)
.where(eq(tradeInTransferDocuments.id, parseInt(req.params.id)));
if (!doc) return res.status(404).json({ error: "Document not found" });
if (!doc.customerSignature) return res.status(400).json({ error: "Customer signature required" });
// Atualizar avaliação para aprovada
await db.update(deviceEvaluations)
.set({ status: "approved", approved: true, updatedAt: sql`CURRENT_TIMESTAMP` })
.where(eq(deviceEvaluations.id, doc.evaluationId));
// Atualizar documento
const [updated] = await db.update(tradeInTransferDocuments)
.set({ status: "completed", updatedAt: sql`CURRENT_TIMESTAMP` })
.where(eq(tradeInTransferDocuments.id, parseInt(req.params.id)))
.returning();
res.json(updated);
} catch (error) {
console.error("Error completing transfer document:", error);
res.status(500).json({ error: "Failed to complete document" });
}
});
// Gerar HTML do documento para impressão/PDF
router.get("/transfer-documents/:id/html", async (req: Request, res: Response) => {
try {
const [doc] = await db.select().from(tradeInTransferDocuments)
.where(eq(tradeInTransferDocuments.id, parseInt(req.params.id)));
if (!doc) return res.status(404).json({ error: "Document not found" });
const html = generateTransferDocumentHTML(doc);
res.setHeader("Content-Type", "text/html");
res.send(html);
} catch (error) {
console.error("Error generating document HTML:", error);
res.status(500).json({ error: "Failed to generate document" });
}
});
function generateTransferDocumentHTML(doc: any): string {
const formatDate = (date: any) => {
if (!date) return new Date().toLocaleDateString("pt-BR");
return new Date(date).toLocaleDateString("pt-BR");
};
const formatCurrency = (value: any) => {
if (!value) return "R$ 0,00";
return new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(parseFloat(value));
};
return `
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Termo de Transferência de Posse - ${doc.documentNumber}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: Arial, sans-serif; font-size: 12px; line-height: 1.5; padding: 20px; max-width: 800px; margin: 0 auto; }
.header { text-align: center; margin-bottom: 30px; border-bottom: 2px solid #333; padding-bottom: 15px; }
.header h1 { font-size: 18px; margin-bottom: 5px; }
.header h2 { font-size: 14px; font-weight: normal; color: #666; }
.doc-number { text-align: right; font-size: 11px; color: #666; margin-bottom: 20px; }
.section { margin-bottom: 20px; }
.section-title { font-size: 13px; font-weight: bold; background: #f0f0f0; padding: 8px; margin-bottom: 10px; }
.field { display: flex; margin-bottom: 8px; }
.field-label { width: 150px; font-weight: bold; }
.field-value { flex: 1; border-bottom: 1px dotted #ccc; padding-bottom: 2px; }
.terms { font-size: 10px; text-align: justify; background: #fafafa; padding: 15px; border: 1px solid #ddd; margin-bottom: 20px; }
.terms h3 { font-size: 11px; margin-bottom: 10px; }
.terms ol { margin-left: 20px; }
.terms li { margin-bottom: 5px; }
.signatures { display: flex; justify-content: space-between; margin-top: 40px; }
.signature-box { width: 45%; text-align: center; }
.signature-line { border-top: 1px solid #333; margin-top: 60px; padding-top: 5px; }
.signature-name { font-weight: bold; }
.signature-doc { font-size: 10px; color: #666; }
.signature-image { max-width: 200px; max-height: 80px; margin: 10px auto; }
.footer { margin-top: 40px; text-align: center; font-size: 10px; color: #666; border-top: 1px solid #ddd; padding-top: 15px; }
.value-box { background: #e8f5e9; padding: 15px; text-align: center; margin: 20px 0; border: 2px solid #4caf50; }
.value-box .amount { font-size: 24px; font-weight: bold; color: #2e7d32; }
@media print {
body { padding: 0; }
.no-print { display: none; }
}
</style>
</head>
<body>
<div class="header">
<h1>TERMO DE TRANSFERÊNCIA DE POSSE</h1>
<h2>Dispositivo Móvel - Trade-In</h2>
</div>
<div class="doc-number">
Documento Nº: <strong>${doc.documentNumber}</strong><br>
Data: ${formatDate(doc.createdAt)}
</div>
<div class="section">
<div class="section-title">DADOS DO CEDENTE (PROPRIETÁRIO ANTERIOR)</div>
<div class="field">
<span class="field-label">Nome Completo:</span>
<span class="field-value">${doc.customerName || ""}</span>
</div>
<div class="field">
<span class="field-label">CPF:</span>
<span class="field-value">${doc.customerCpf || ""}</span>
</div>
<div class="field">
<span class="field-label">RG:</span>
<span class="field-value">${doc.customerRg || ""}</span>
</div>
<div class="field">
<span class="field-label">Telefone:</span>
<span class="field-value">${doc.customerPhone || ""}</span>
</div>
<div class="field">
<span class="field-label">E-mail:</span>
<span class="field-value">${doc.customerEmail || ""}</span>
</div>
<div class="field">
<span class="field-label">Endereço:</span>
<span class="field-value">${doc.customerAddress || ""}</span>
</div>
</div>
<div class="section">
<div class="section-title">DADOS DO DISPOSITIVO</div>
<div class="field">
<span class="field-label">Marca:</span>
<span class="field-value">${doc.deviceBrand}</span>
</div>
<div class="field">
<span class="field-label">Modelo:</span>
<span class="field-value">${doc.deviceModel}</span>
</div>
<div class="field">
<span class="field-label">IMEI Principal:</span>
<span class="field-value">${doc.deviceImei}</span>
</div>
${doc.deviceImei2 ? `
<div class="field">
<span class="field-label">IMEI Secundário:</span>
<span class="field-value">${doc.deviceImei2}</span>
</div>
` : ""}
<div class="field">
<span class="field-label">Cor:</span>
<span class="field-value">${doc.deviceColor || ""}</span>
</div>
<div class="field">
<span class="field-label">Armazenamento:</span>
<span class="field-value">${doc.deviceStorage || ""}</span>
</div>
<div class="field">
<span class="field-label">Condição:</span>
<span class="field-value">${doc.deviceCondition || ""}</span>
</div>
</div>
<div class="value-box">
<div>Valor acordado para transferência:</div>
<div class="amount">${formatCurrency(doc.agreedValue)}</div>
<div style="font-size: 11px; color: #666;">Forma de pagamento: ${doc.paymentMethod === "credit" ? "Crédito em conta" : doc.paymentMethod === "discount_on_purchase" ? "Desconto na compra" : "Dinheiro"}</div>
</div>
<div class="terms">
<h3>TERMOS E CONDIÇÕES</h3>
<ol>
<li>O CEDENTE declara ser o legítimo proprietário do dispositivo descrito acima e que o mesmo não possui qualquer restrição de uso, pendência financeira, bloqueio por roubo/furto ou gravame de qualquer natureza.</li>
<li>O CEDENTE declara que o dispositivo não é objeto de financiamento em andamento e não está vinculado a nenhum contrato de operadora que impeça sua transferência.</li>
<li>O CEDENTE transfere todos os direitos de posse e propriedade do dispositivo para a empresa CESSIONÁRIA.</li>
<li>O CEDENTE compromete-se a remover todas as contas vinculadas ao dispositivo (iCloud, Google, Samsung, etc.) antes da entrega.</li>
<li>A CESSIONÁRIA realizou avaliação técnica do dispositivo e ambas as partes concordam com o valor acordado.</li>
<li>O CEDENTE autoriza a CESSIONÁRIA a revender, locar ou utilizar o dispositivo da forma que melhor lhe convier.</li>
<li>O CEDENTE assume total responsabilidade por qualquer declaração falsa prestada neste termo.</li>
</ol>
</div>
<div class="signatures">
<div class="signature-box">
${doc.customerSignature ? `<img src="${doc.customerSignature}" class="signature-image" alt="Assinatura do Cliente">` : ""}
<div class="signature-line">
<div class="signature-name">${doc.customerName}</div>
<div class="signature-doc">CPF: ${doc.customerCpf || "___.___.___-__"}</div>
<div class="signature-doc">CEDENTE</div>
</div>
</div>
<div class="signature-box">
${doc.employeeSignature ? `<img src="${doc.employeeSignature}" class="signature-image" alt="Assinatura do Funcionário">` : ""}
<div class="signature-line">
<div class="signature-name">${doc.employeeName || "____________________"}</div>
<div class="signature-doc">REPRESENTANTE DA EMPRESA</div>
<div class="signature-doc">CESSIONÁRIA</div>
</div>
</div>
</div>
<div class="footer">
<p>Documento gerado eletronicamente pelo sistema Arcádia Retail</p>
<p>Data de geração: ${new Date().toLocaleString("pt-BR")}</p>
</div>
</body>
</html>
`;
}
// ========== PHASE 0: PERSONS (Unified Person Registry) ==========
// List all persons with optional role filter
router.get("/persons", async (req: Request, res: Response) => {
try {
const { search, role, isActive } = req.query;
let result = await db.select().from(persons).orderBy(desc(persons.createdAt));
// Apply filters in memory for now (can be optimized later)
if (search) {
const searchLower = (search as string).toLowerCase();
result = result.filter(p =>
p.fullName.toLowerCase().includes(searchLower) ||
(p.cpfCnpj && p.cpfCnpj.includes(search as string)) ||
(p.email && p.email.toLowerCase().includes(searchLower)) ||
(p.phone && p.phone.includes(search as string))
);
}
if (isActive !== undefined) {
result = result.filter(p => p.isActive === (isActive === "true"));
}
// If role filter, get person IDs with that role
if (role) {
const rolesWithType = await db.select().from(personRoles)
.where(eq(personRoles.roleType, role as string));
const personIdsWithRole = rolesWithType.map(r => r.personId);
result = result.filter(p => personIdsWithRole.includes(p.id));
}
// Fetch roles for each person
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 };
}));
res.json(personsWithRoles);
} catch (error) {
console.error("Error fetching persons:", error);
res.status(500).json({ error: "Failed to fetch persons" });
}
});
// Get single person with all roles
router.get("/persons/:id", async (req: Request, res: Response) => {
try {
const [person] = await db.select().from(persons)
.where(eq(persons.id, parseInt(req.params.id)));
if (!person) {
return res.status(404).json({ error: "Person not found" });
}
const roles = await db.select().from(personRoles)
.where(eq(personRoles.personId, person.id));
res.json({ ...person, roles });
} catch (error) {
console.error("Error fetching person:", error);
res.status(500).json({ error: "Failed to fetch person" });
}
});
// Search persons by name/cpf
router.get("/persons/search", async (req: Request, res: Response) => {
try {
const { q } = req.query;
if (!q || (q as string).length < 2) {
return res.json([]);
}
const result = await db.select().from(persons)
.where(or(
ilike(persons.fullName, `%${q}%`),
ilike(persons.cpfCnpj, `%${q}%`)
))
.limit(20);
// Fetch roles
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) };
}));
res.json(personsWithRoles);
} catch (error) {
console.error("Error searching persons:", error);
res.status(500).json({ error: "Failed to search persons" });
}
});
// Create new person
router.post("/persons", async (req: Request, res: Response) => {
try {
const { roles: rolesData, ...personData } = req.body;
const [person] = await db.insert(persons).values(personData).returning();
// Create roles if provided
if (rolesData && Array.isArray(rolesData) && rolesData.length > 0) {
for (const roleData of rolesData) {
await db.insert(personRoles).values({
personId: person.id,
...roleData
});
}
}
// Fetch created roles
const createdRoles = await db.select().from(personRoles)
.where(eq(personRoles.personId, person.id));
res.json({ ...person, roles: createdRoles });
} catch (error) {
console.error("Error creating person:", error);
res.status(500).json({ error: "Failed to create person" });
}
});
// Update person
router.put("/persons/:id", async (req: Request, res: Response) => {
try {
const { roles: rolesData, ...personData } = req.body;
const [person] = await db.update(persons)
.set({ ...personData, updatedAt: sql`CURRENT_TIMESTAMP` })
.where(eq(persons.id, parseInt(req.params.id)))
.returning();
res.json(person);
} catch (error) {
console.error("Error updating person:", error);
res.status(500).json({ error: "Failed to update person" });
}
});
// Add role to person
router.post("/persons/:id/roles", async (req: Request, res: Response) => {
try {
const personId = parseInt(req.params.id);
// Check if person exists
const [person] = await db.select().from(persons).where(eq(persons.id, personId));
if (!person) {
return res.status(404).json({ error: "Person not found" });
}
// Check if role already exists
const existingRole = await db.select().from(personRoles)
.where(and(
eq(personRoles.personId, personId),
eq(personRoles.roleType, req.body.roleType)
));
if (existingRole.length > 0) {
return res.status(400).json({ error: "Role already exists for this person" });
}
const [role] = await db.insert(personRoles).values({
personId,
...req.body
}).returning();
res.json(role);
} catch (error) {
console.error("Error adding role:", error);
res.status(500).json({ error: "Failed to add role" });
}
});
// Update role
router.put("/persons/:id/roles/:roleId", async (req: Request, res: Response) => {
try {
const [role] = await db.update(personRoles)
.set({ ...req.body, updatedAt: sql`CURRENT_TIMESTAMP` })
.where(eq(personRoles.id, parseInt(req.params.roleId)))
.returning();
res.json(role);
} catch (error) {
console.error("Error updating role:", error);
res.status(500).json({ error: "Failed to update role" });
}
});
// Remove role from person
router.delete("/persons/:id/roles/:roleType", async (req: Request, res: Response) => {
try {
await db.delete(personRoles)
.where(and(
eq(personRoles.personId, parseInt(req.params.id)),
eq(personRoles.roleType, req.params.roleType)
));
res.json({ success: true });
} catch (error) {
console.error("Error removing role:", error);
res.status(500).json({ error: "Failed to remove role" });
}
});
// ========== PHASE 0: TRADE-IN FLOW (Approve → Internal OS → Stock) ==========
// Generate unique order number
function generateOrderNumber(prefix: string = "OS"): string {
const date = new Date();
const year = date.getFullYear().toString().slice(-2);
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const random = Math.random().toString(36).substring(2, 8).toUpperCase();
return `${prefix}${year}${month}${random}`;
}
// Approve Trade-In and create Internal Service Order
router.post("/evaluations/:id/approve-and-process", async (req: Request, res: Response) => {
try {
const evaluationId = parseInt(req.params.id);
const { profitMargin = 30 } = req.body; // Default 30% margin
// Get evaluation
const [evaluation] = await db.select().from(deviceEvaluations)
.where(eq(deviceEvaluations.id, evaluationId));
if (!evaluation) {
return res.status(404).json({ error: "Evaluation not found" });
}
if (evaluation.status === "approved") {
return res.status(400).json({ error: "Evaluation already approved" });
}
// 1. Update evaluation to approved
await db.update(deviceEvaluations)
.set({
status: "approved",
approved: true,
updatedAt: sql`CURRENT_TIMESTAMP`
})
.where(eq(deviceEvaluations.id, evaluationId));
// 2. Create Internal Service Order for revision
const orderNumber = generateOrderNumber("INT");
const [serviceOrder] = await db.insert(serviceOrders).values({
tenantId: evaluation.tenantId,
orderNumber,
storeId: evaluation.storeId,
imei: evaluation.imei,
brand: evaluation.brand,
model: evaluation.model,
customerName: "ORDEM INTERNA - REVISÃO",
serviceType: "internal_review",
issueDescription: `Revisão e manutenção de dispositivo Trade-In. Avaliação #${evaluationId}`,
origin: "device_acquisition",
status: "open",
priority: "normal",
isInternal: true,
internalType: "revision",
sourceEvaluationId: evaluationId,
}).returning();
// 3. Record in IMEI history
await db.insert(imeiHistory).values({
tenantId: evaluation.tenantId,
deviceId: null as any,
imei: evaluation.imei,
action: "trade_in_approved",
newStatus: "in_revision",
relatedOrderId: serviceOrder.id,
relatedOrderType: "service_order",
relatedOrderNumber: orderNumber,
notes: `Trade-In aprovado. Valor: R$ ${evaluation.estimatedValue}. O.S. Interna criada.`,
createdBy: (req.user as any)?.id,
createdByName: (req.user as any)?.name || "Sistema",
});
// 4. Generate customer credit if person and value are valid
let credit = null;
const personId = evaluation.personId;
const estimatedValueNum = evaluation.estimatedValue ? parseFloat(evaluation.estimatedValue) : 0;
if (personId && estimatedValueNum > 0) {
let customerCpf = null;
let customerName = evaluation.customerName || "Cliente";
const [person] = await db.select().from(persons)
.where(eq(persons.id, personId))
.limit(1);
if (person) {
customerCpf = person.cpfCnpj;
customerName = person.fullName;
}
[credit] = await db.insert(customerCredits).values({
storeId: evaluation.storeId || undefined,
personId: personId,
customerName,
customerCpf: customerCpf || undefined,
amount: evaluation.estimatedValue!,
remainingAmount: evaluation.estimatedValue!,
origin: "trade_in",
originId: evaluation.id,
description: `Trade-In: ${evaluation.brand} ${evaluation.model} (IMEI: ${evaluation.imei})`,
status: "active",
createdBy: (req.user as any)?.id
}).returning();
await db.update(deviceEvaluations)
.set({ creditGenerated: true, creditId: credit.id })
.where(eq(deviceEvaluations.id, evaluationId));
}
res.json({
success: true,
message: "Trade-In aprovado e O.S. Interna criada com sucesso",
evaluation: { ...evaluation, status: "approved" },
serviceOrder,
credit,
nextStep: "Realize a revisão/manutenção do dispositivo e finalize a O.S. para entrada no estoque"
});
} catch (error) {
console.error("Error processing trade-in:", error);
res.status(500).json({ error: "Failed to process trade-in" });
}
});
// Get full Trade-In flow status
router.get("/evaluations/:id/full-flow", async (req: Request, res: Response) => {
try {
const evaluationId = parseInt(req.params.id);
// Get evaluation
const [evaluation] = await db.select().from(deviceEvaluations)
.where(eq(deviceEvaluations.id, evaluationId));
if (!evaluation) {
return res.status(404).json({ error: "Evaluation not found" });
}
// Get related internal OS
const relatedOS = await db.select().from(serviceOrders)
.where(eq(serviceOrders.sourceEvaluationId, evaluationId));
// Get related device in stock (if exists)
const relatedDevice = await db.select().from(mobileDevices)
.where(eq(mobileDevices.relatedEvaluationId, evaluationId));
// Get IMEI history
const history = await db.select().from(imeiHistory)
.where(eq(imeiHistory.imei, evaluation.imei))
.orderBy(desc(imeiHistory.createdAt));
// Determine flow status
let flowStatus = "pending";
let flowStep = 1;
if (evaluation.status === "approved") {
flowStatus = "approved";
flowStep = 2;
}
if (relatedOS.length > 0 && relatedOS[0].status === "completed") {
flowStatus = "revision_completed";
flowStep = 3;
}
if (relatedDevice.length > 0) {
flowStatus = "in_stock";
flowStep = 4;
}
res.json({
evaluation,
serviceOrder: relatedOS[0] || null,
device: relatedDevice[0] || null,
history,
flowStatus,
flowStep,
steps: [
{ step: 1, name: "Avaliação", status: "completed" },
{ step: 2, name: "Aprovação", status: flowStep >= 2 ? "completed" : "pending" },
{ step: 3, name: "Revisão (O.S.)", status: flowStep >= 3 ? "completed" : flowStep === 2 ? "in_progress" : "pending" },
{ step: 4, name: "Estoque", status: flowStep >= 4 ? "completed" : "pending" },
]
});
} catch (error) {
console.error("Error fetching trade-in flow:", error);
res.status(500).json({ error: "Failed to fetch trade-in flow" });
}
});
// Finalize Internal OS and create device in stock
router.post("/service-orders/:id/finalize-internal", async (req: Request, res: Response) => {
try {
const orderId = parseInt(req.params.id);
const { profitMargin = 30, warehouseId, notes } = req.body;
// Get service order
const [serviceOrder] = await db.select().from(serviceOrders)
.where(eq(serviceOrders.id, orderId));
if (!serviceOrder) {
return res.status(404).json({ error: "Service order not found" });
}
if (!serviceOrder.isInternal) {
return res.status(400).json({ error: "This is not an internal service order" });
}
if (serviceOrder.status === "completed") {
return res.status(400).json({ error: "Service order already completed" });
}
// Get original evaluation
const [evaluation] = await db.select().from(deviceEvaluations)
.where(eq(deviceEvaluations.id, serviceOrder.sourceEvaluationId!));
if (!evaluation) {
return res.status(400).json({ error: "Original evaluation not found" });
}
// Calculate costs
const tradeInValue = parseFloat(evaluation.estimatedValue?.toString() || "0");
const osCost = parseFloat(serviceOrder.totalCost?.toString() || "0");
const acquisitionCost = tradeInValue + osCost;
const suggestedPrice = acquisitionCost * (1 + profitMargin / 100);
// 1. Update service order to completed
await db.update(serviceOrders)
.set({
status: "completed",
actualCompletionDate: sql`CURRENT_DATE`,
updatedAt: sql`CURRENT_TIMESTAMP`
})
.where(eq(serviceOrders.id, orderId));
// 2. Create device in stock
const [device] = await db.insert(mobileDevices).values({
tenantId: serviceOrder.tenantId,
imei: serviceOrder.imei,
brand: serviceOrder.brand || evaluation.brand,
model: serviceOrder.model || evaluation.model,
color: evaluation.color,
condition: "refurbished",
warehouseId: warehouseId || null,
storeId: serviceOrder.storeId,
status: "in_stock",
purchaseDate: sql`CURRENT_DATE`,
purchasePrice: acquisitionCost.toString(),
sellingPrice: suggestedPrice.toString(),
acquisitionType: "trade_in",
acquisitionCost: acquisitionCost.toString(),
relatedEvaluationId: evaluation.id,
relatedServiceOrderId: orderId,
suggestedPrice: suggestedPrice.toString(),
profitMargin: profitMargin.toString(),
notes: notes || `Origem: Trade-In #${evaluation.id}. Revisão: O.S. ${serviceOrder.orderNumber}`,
}).returning();
// 3. Update evaluation with device reference
await db.update(deviceEvaluations)
.set({ deviceId: device.id, updatedAt: sql`CURRENT_TIMESTAMP` })
.where(eq(deviceEvaluations.id, evaluation.id));
// 4. Record in IMEI history
await db.insert(imeiHistory).values({
tenantId: serviceOrder.tenantId,
deviceId: device.id,
imei: serviceOrder.imei,
action: "stock_entry",
previousStatus: "in_revision",
newStatus: "in_stock",
newLocation: warehouseId ? `Warehouse #${warehouseId}` : `Store #${serviceOrder.storeId}`,
relatedOrderId: orderId,
relatedOrderType: "service_order",
relatedOrderNumber: serviceOrder.orderNumber,
cost: acquisitionCost.toString(),
notes: `Dispositivo entrou no estoque. Custo: R$ ${acquisitionCost.toFixed(2)}. Preço sugerido: R$ ${suggestedPrice.toFixed(2)} (margem ${profitMargin}%)`,
createdBy: (req.user as any)?.id,
createdByName: (req.user as any)?.name || "Sistema",
});
res.json({
success: true,
message: "O.S. finalizada e dispositivo adicionado ao estoque",
device,
costs: {
tradeInValue,
osCost,
totalAcquisitionCost: acquisitionCost,
suggestedSellingPrice: suggestedPrice,
profitMargin: `${profitMargin}%`
}
});
} catch (error) {
console.error("Error finalizing internal OS:", error);
res.status(500).json({ error: "Failed to finalize internal service order" });
}
});
// ========== PHASE 0: IMEI HISTORY (Kardex) ==========
// Get IMEI history
router.get("/devices/:imei/history", async (req: Request, res: Response) => {
try {
const history = await db.select().from(imeiHistory)
.where(eq(imeiHistory.imei, req.params.imei))
.orderBy(desc(imeiHistory.createdAt));
res.json(history);
} catch (error) {
console.error("Error fetching IMEI history:", error);
res.status(500).json({ error: "Failed to fetch IMEI history" });
}
});
// Get devices by origin type
router.get("/devices/by-origin/:originType", async (req: Request, res: Response) => {
try {
const devices = await db.select().from(mobileDevices)
.where(eq(mobileDevices.acquisitionType, req.params.originType))
.orderBy(desc(mobileDevices.createdAt));
res.json(devices);
} catch (error) {
console.error("Error fetching devices by origin:", error);
res.status(500).json({ error: "Failed to fetch devices by origin" });
}
});
// Get stock summary by origin
router.get("/inventory/by-origin", async (req: Request, res: Response) => {
try {
const devices = await db.select().from(mobileDevices)
.where(eq(mobileDevices.status, "in_stock"));
// Group by origin
const summary: Record<string, { count: number; totalValue: number; avgMargin: number }> = {};
for (const device of devices) {
const origin = device.acquisitionType || "unknown";
if (!summary[origin]) {
summary[origin] = { count: 0, totalValue: 0, avgMargin: 0 };
}
summary[origin].count++;
summary[origin].totalValue += parseFloat(device.sellingPrice?.toString() || "0");
summary[origin].avgMargin += parseFloat(device.profitMargin?.toString() || "0");
}
// Calculate average margins
for (const origin in summary) {
if (summary[origin].count > 0) {
summary[origin].avgMargin /= summary[origin].count;
}
}
res.json(summary);
} catch (error) {
console.error("Error fetching inventory by origin:", error);
res.status(500).json({ error: "Failed to fetch inventory by origin" });
}
});
// ========== ERPNEXT SYNC ENDPOINTS ==========
import { retailSyncService } from "./sync-service";
import * as erpnextService from "../erpnext/service";
// Check ERPNext connection status
router.get("/sync/status", async (req: Request, res: Response) => {
try {
const config = erpnextService.getConfig();
if (!config.configured) {
return res.json({
connected: false,
message: "ERPNext não configurado. Configure as credenciais nas secrets."
});
}
const result = await erpnextService.testConnection();
return res.json({
connected: result.success,
message: result.message,
url: config.url,
});
} catch (error) {
console.error("Error checking sync status:", error);
res.status(500).json({ connected: false, message: "Erro ao verificar conexão" });
}
});
// Sync a single person to ERPNext
router.post("/sync/persons/:id", async (req: Request, res: Response) => {
try {
const personId = parseInt(req.params.id);
const result = await retailSyncService.syncPersonToERPNext(personId);
res.json(result);
} catch (error) {
console.error("Error syncing person:", error);
res.status(500).json({ success: false, error: "Failed to sync person" });
}
});
// Sync all persons to ERPNext
router.post("/sync/persons", async (req: Request, res: Response) => {
try {
const result = await retailSyncService.syncAllPersonsToERPNext();
res.json(result);
} catch (error) {
console.error("Error syncing all persons:", error);
res.status(500).json({ success: false, error: "Failed to sync persons" });
}
});
// Sync a single device to ERPNext
router.post("/sync/devices/:id", async (req: Request, res: Response) => {
try {
const deviceId = parseInt(req.params.id);
const result = await retailSyncService.syncDeviceToERPNext(deviceId);
res.json(result);
} catch (error) {
console.error("Error syncing device:", error);
res.status(500).json({ success: false, error: "Failed to sync device" });
}
});
// Sync a service order to ERPNext
router.post("/sync/service-orders/:id", async (req: Request, res: Response) => {
try {
const orderId = parseInt(req.params.id);
const result = await retailSyncService.syncServiceOrderToERPNext(orderId);
res.json(result);
} catch (error) {
console.error("Error syncing service order:", error);
res.status(500).json({ success: false, error: "Failed to sync service order" });
}
});
// Import customers from ERPNext
router.post("/sync/import/customers", async (req: Request, res: Response) => {
try {
const limit = parseInt(req.query.limit as string) || 100;
const result = await retailSyncService.importCustomersFromERPNext(limit);
res.json(result);
} catch (error) {
console.error("Error importing customers:", error);
res.status(500).json({ success: false, error: "Failed to import customers" });
}
});
// Import suppliers from ERPNext
router.post("/sync/import/suppliers", async (req: Request, res: Response) => {
try {
const limit = parseInt(req.query.limit as string) || 100;
const result = await retailSyncService.importSuppliersFromERPNext(limit);
res.json(result);
} catch (error) {
console.error("Error importing suppliers:", error);
res.status(500).json({ success: false, error: "Failed to import suppliers" });
}
});
// Run full sync
router.post("/sync/full", async (req: Request, res: Response) => {
try {
const result = await retailSyncService.runFullSync();
res.json(result);
} catch (error) {
console.error("Error running full sync:", error);
res.status(500).json({ success: false, error: "Failed to run full sync" });
}
});
// Create stock entry in ERPNext
router.post("/sync/stock-entry", async (req: Request, res: Response) => {
try {
const { entryType, items, fromWarehouse, toWarehouse } = req.body;
const result = await retailSyncService.createStockEntry(entryType, items, fromWarehouse, toWarehouse);
res.json(result);
} catch (error) {
console.error("Error creating stock entry:", error);
res.status(500).json({ success: false, error: "Failed to create stock entry" });
}
});
// Create sales invoice in ERPNext
router.post("/sync/sales-invoice", async (req: Request, res: Response) => {
try {
const { customerName, items, paymentMode } = req.body;
const result = await retailSyncService.createSalesInvoice(customerName, items, paymentMode);
res.json(result);
} catch (error) {
console.error("Error creating sales invoice:", error);
res.status(500).json({ success: false, error: "Failed to create sales invoice" });
}
});
// ========== CUSTOMER CREDITS ==========
router.get("/credits", async (req: Request, res: Response) => {
try {
const { personId, status } = req.query;
let query = db.select().from(customerCredits);
const conditions = [];
if (personId) conditions.push(eq(customerCredits.personId, parseInt(personId as string)));
if (status) conditions.push(eq(customerCredits.status, status as string));
const credits = conditions.length > 0
? await query.where(and(...conditions)).orderBy(desc(customerCredits.createdAt))
: await query.orderBy(desc(customerCredits.createdAt));
res.json(credits);
} catch (error) {
console.error("Error fetching credits:", error);
res.status(500).json({ error: "Failed to fetch credits" });
}
});
router.get("/credits/by-person/:personId", async (req: Request, res: Response) => {
try {
const credits = await db.select().from(customerCredits)
.where(and(
eq(customerCredits.personId, parseInt(req.params.personId)),
eq(customerCredits.status, "active")
))
.orderBy(desc(customerCredits.createdAt));
const totalAvailable = credits.reduce((sum, c) => sum + parseFloat(c.remainingAmount || "0"), 0);
res.json({ credits, totalAvailable });
} catch (error) {
console.error("Error fetching credits by person:", error);
res.status(500).json({ error: "Failed to fetch credits" });
}
});
router.post("/credits/:id/use", async (req: Request, res: Response) => {
try {
const { amount, saleId } = req.body;
const [credit] = await db.select().from(customerCredits)
.where(eq(customerCredits.id, parseInt(req.params.id)));
if (!credit) return res.status(404).json({ error: "Credit not found" });
if (credit.status !== "active") return res.status(400).json({ error: "Credit is not active" });
const remaining = parseFloat(credit.remainingAmount || "0");
const useAmount = Math.min(parseFloat(amount), remaining);
const newRemaining = remaining - useAmount;
const newUsed = parseFloat(credit.usedAmount || "0") + useAmount;
const [updated] = await db.update(customerCredits)
.set({
usedAmount: newUsed.toFixed(2),
remainingAmount: newRemaining.toFixed(2),
status: newRemaining <= 0 ? "used" : "active",
usedInSaleId: saleId || undefined,
updatedAt: new Date()
})
.where(eq(customerCredits.id, parseInt(req.params.id)))
.returning();
res.json({ credit: updated, amountUsed: useAmount });
} catch (error) {
console.error("Error using credit:", error);
res.status(500).json({ error: "Failed to use credit" });
}
});
router.get("/evaluations/:id", async (req: Request, res: Response) => {
try {
const [evaluation] = await db.select().from(deviceEvaluations)
.where(eq(deviceEvaluations.id, parseInt(req.params.id)));
if (!evaluation) return res.status(404).json({ error: "Evaluation not found" });
res.json(evaluation);
} catch (error) {
console.error("Error fetching evaluation:", error);
res.status(500).json({ error: "Failed to fetch evaluation" });
}
});
// ========== PERSON HISTORY ==========
router.get("/persons/:id/sales", async (req: Request, res: Response) => {
try {
const personId = parseInt(req.params.id);
const sales = await db.select().from(posSales)
.where(eq(posSales.customerId, String(personId)))
.orderBy(desc(posSales.createdAt));
res.json(sales);
} catch (error) {
console.error("Error fetching person sales:", error);
res.status(500).json({ error: "Failed to fetch person sales" });
}
});
router.get("/persons/:id/services", async (req: Request, res: Response) => {
try {
const personId = parseInt(req.params.id);
const services = await db.select().from(serviceOrders)
.where(eq(serviceOrders.personId, personId))
.orderBy(desc(serviceOrders.createdAt));
res.json(services);
} catch (error) {
console.error("Error fetching person services:", error);
res.status(500).json({ error: "Failed to fetch person services" });
}
});
router.get("/persons/:id/trade-ins", async (req: Request, res: Response) => {
try {
const personId = parseInt(req.params.id);
const tradeIns = await db.select().from(deviceEvaluations)
.where(eq(deviceEvaluations.personId, personId))
.orderBy(desc(deviceEvaluations.createdAt));
res.json(tradeIns);
} catch (error) {
console.error("Error fetching person trade-ins:", error);
res.status(500).json({ error: "Failed to fetch person trade-ins" });
}
});
router.get("/persons/:id/credits", async (req: Request, res: Response) => {
try {
const personId = parseInt(req.params.id);
const credits = await db.select().from(customerCredits)
.where(eq(customerCredits.personId, personId))
.orderBy(desc(customerCredits.createdAt));
res.json(credits);
} catch (error) {
console.error("Error fetching person credits:", error);
res.status(500).json({ error: "Failed to fetch person credits" });
}
});
// ========== TRADE-IN WORKFLOW - STATUS UPDATES ==========
// Alterar status para "Em Análise"
router.put("/evaluations/:id/start-analysis", async (req: Request, res: Response) => {
try {
const [evaluation] = await db.update(deviceEvaluations)
.set({
status: "analyzing",
diagnosisStatus: "in_progress",
evaluatedBy: (req as any).user?.id,
updatedAt: new Date()
})
.where(eq(deviceEvaluations.id, parseInt(req.params.id)))
.returning();
res.json(evaluation);
} catch (error) {
console.error("Error starting analysis:", error);
res.status(500).json({ error: "Failed to start analysis" });
}
});
// Buscar trade-ins ativos do cliente (para alertas no PDV)
router.get("/customer-trade-ins/:personId", async (req: Request, res: Response) => {
try {
const personId = parseInt(req.params.personId);
const tradeIns = await db.select().from(deviceEvaluations)
.where(and(
eq(deviceEvaluations.personId, personId),
or(
eq(deviceEvaluations.status, "pending"),
eq(deviceEvaluations.status, "analyzing"),
eq(deviceEvaluations.status, "approved")
)
))
.orderBy(desc(deviceEvaluations.createdAt));
// Buscar créditos disponíveis
const credits = await db.select().from(customerCredits)
.where(and(
eq(customerCredits.personId, personId),
eq(customerCredits.status, "active")
));
const totalCredit = credits.reduce((sum, c) => sum + parseFloat(c.remainingAmount || "0"), 0);
res.json({ tradeIns, credits, totalCredit });
} catch (error) {
console.error("Error fetching customer trade-ins:", error);
res.status(500).json({ error: "Failed to fetch customer trade-ins" });
}
});
// ========== MANAGER PASSWORD VERIFICATION ==========
router.post("/verify-manager-password", async (req: Request, res: Response) => {
try {
const { password, action } = req.body;
// Senha de gerente padrão (deve ser configurável no futuro)
const MANAGER_PASSWORD = process.env.MANAGER_PASSWORD || "gerente123";
if (password === MANAGER_PASSWORD) {
res.json({ authorized: true, action });
} else {
res.status(401).json({ authorized: false, error: "Senha incorreta" });
}
} catch (error) {
console.error("Error verifying manager password:", error);
res.status(500).json({ error: "Failed to verify password" });
}
});
// ========== RETURNS/REFUNDS ==========
// Buscar vendas por cliente ou IMEI para devolução
router.get("/sales-for-return", async (req: Request, res: Response) => {
try {
const { personId, imei, search } = req.query;
let conditions = [];
if (personId) {
conditions.push(eq(posSales.customerId, String(personId)));
}
if (search) {
conditions.push(or(
ilike(posSales.customerName!, `%${search}%`),
ilike(posSales.saleNumber!, `%${search}%`)
));
}
if (conditions.length === 0) {
return res.json([]);
}
const sales = await db.select().from(posSales)
.where(and(...conditions, eq(posSales.status, "completed")))
.orderBy(desc(posSales.createdAt))
.limit(20);
// Para cada venda, buscar itens
const salesWithItems = await Promise.all(sales.map(async (sale) => {
const items = await db.select().from(posSaleItems)
.where(eq(posSaleItems.saleId, sale.id));
return { ...sale, items };
}));
// Filtrar por IMEI se especificado
if (imei) {
const filtered = salesWithItems.filter(s =>
s.items.some((i: any) => i.imei?.toLowerCase().includes((imei as string).toLowerCase()))
);
return res.json(filtered);
}
res.json(salesWithItems);
} catch (error) {
console.error("Error fetching sales for return:", error);
res.status(500).json({ error: "Failed to fetch sales" });
}
});
// Processar devolução e gerar crédito
router.post("/returns", async (req: Request, res: Response) => {
try {
const { saleId, items, reason, personId, customerName, generateCredit } = req.body;
if (!items || items.length === 0) {
return res.status(400).json({ error: "Nenhum item selecionado para devolução" });
}
// Buscar a venda original
const [originalSale] = await db.select().from(posSales)
.where(eq(posSales.id, saleId));
if (!originalSale) {
return res.status(404).json({ error: "Venda não encontrada" });
}
// Calcular valor total da devolução
let totalReturn = 0;
for (const item of items) {
totalReturn += parseFloat(item.totalPrice || item.unitPrice || "0");
}
// Criar registro de devolução
const returnNumber = `DEV-${new Date().getFullYear()}-${String(Date.now()).slice(-6)}`;
const [returnRecord] = await db.insert(returnExchanges).values({
returnNumber,
originalSaleId: saleId,
customerId: personId?.toString(),
customerName: customerName || originalSale.customerName,
returnType: "return",
reason,
refundAmount: String(totalReturn),
status: "processed",
processedDate: sql`CURRENT_DATE`,
processedBy: (req as any).user?.id
}).returning();
// Processar cada item devolvido
for (const item of items) {
await db.insert(returnExchangeItems).values({
returnId: returnRecord.id,
deviceId: item.deviceId,
imei: item.imei,
itemName: item.itemName,
quantity: item.quantity || 1,
refundAmount: String(item.totalPrice || item.unitPrice || "0"),
reason: reason
});
// Se for dispositivo, retornar ao estoque
if (item.deviceId) {
await db.update(mobileDevices)
.set({
status: "in_stock",
soldDate: null,
soldToCustomer: null,
updatedAt: sql`CURRENT_TIMESTAMP`
})
.where(eq(mobileDevices.id, item.deviceId));
await db.insert(deviceHistory).values({
deviceId: item.deviceId,
imei: item.imei,
eventType: "returned",
fromLocation: customerName || "Cliente",
toLocation: `Estoque - Loja ${originalSale.storeId || 1}`,
referenceType: "return",
referenceId: returnRecord.id,
notes: `Devolução: ${reason || 'Sem motivo especificado'}`,
createdBy: (req as any).user?.id
});
}
}
// Gerar crédito para o cliente se solicitado
let credit = null;
if (generateCredit && personId && totalReturn > 0) {
[credit] = await db.insert(customerCredits).values({
storeId: originalSale.storeId,
personId: parseInt(personId),
customerName: customerName || originalSale.customerName || "Cliente",
amount: String(totalReturn),
remainingAmount: String(totalReturn),
origin: "refund",
originId: returnRecord.id,
description: `Devolução ref. Venda ${originalSale.saleNumber}`,
status: "active",
createdBy: (req as any).user?.id
}).returning();
}
res.json({
returnRecord,
credit,
message: credit
? `Devolução processada. Crédito de R$ ${totalReturn.toFixed(2)} gerado para o cliente.`
: "Devolução processada com sucesso."
});
} catch (error) {
console.error("Error processing return:", error);
res.status(500).json({ error: "Failed to process return" });
}
});
// Buscar O.S. vinculada a uma avaliação Trade-In
router.get("/evaluations/:id/service-order", async (req: Request, res: Response) => {
try {
const evalId = parseInt(req.params.id);
const [order] = await db.select().from(serviceOrders)
.where(eq(serviceOrders.sourceEvaluationId, evalId));
if (!order) {
return res.status(404).json({ error: "O.S. não encontrada" });
}
res.json(order);
} catch (error) {
console.error("Error fetching service order for evaluation:", error);
res.status(500).json({ error: "Failed to fetch service order" });
}
});
// Salvar checklist na O.S.
router.put("/service-orders/:id/checklist", async (req: Request, res: Response) => {
try {
const { checklistData, completedBy } = req.body;
const [order] = await db.update(serviceOrders)
.set({
checklistData,
checklistCompletedAt: completedBy ? sql`CURRENT_TIMESTAMP` : null,
checklistCompletedBy: completedBy,
updatedAt: sql`CURRENT_TIMESTAMP`
})
.where(eq(serviceOrders.id, parseInt(req.params.id)))
.returning();
res.json(order);
} catch (error) {
console.error("Error updating checklist:", error);
res.status(500).json({ error: "Failed to update checklist" });
}
});
// ========== METAS DE VENDEDOR ==========
router.get("/seller-goals", async (req: Request, res: Response) => {
try {
const { sellerId, storeId, month, year } = req.query;
const conditions = [];
if (sellerId) conditions.push(eq(retailSellerGoals.sellerId, parseInt(sellerId as string)));
if (storeId) conditions.push(eq(retailSellerGoals.storeId, parseInt(storeId as string)));
if (month) conditions.push(eq(retailSellerGoals.month, parseInt(month as string)));
if (year) conditions.push(eq(retailSellerGoals.year, parseInt(year as string)));
const goals = conditions.length > 0
? await db.select().from(retailSellerGoals).where(and(...conditions))
: await db.select().from(retailSellerGoals);
res.json(goals);
} catch (error) {
console.error("Error fetching seller goals:", error);
res.status(500).json({ error: "Failed to fetch seller goals" });
}
});
router.post("/seller-goals", async (req: Request, res: Response) => {
try {
const [goal] = await db.insert(retailSellerGoals).values(req.body).returning();
res.status(201).json(goal);
} catch (error) {
console.error("Error creating seller goal:", error);
res.status(500).json({ error: "Failed to create seller goal" });
}
});
router.put("/seller-goals/:id", async (req: Request, res: Response) => {
try {
const [goal] = await db.update(retailSellerGoals)
.set({ ...req.body, updatedAt: sql`CURRENT_TIMESTAMP` })
.where(eq(retailSellerGoals.id, parseInt(req.params.id)))
.returning();
res.json(goal);
} catch (error) {
console.error("Error updating seller goal:", error);
res.status(500).json({ error: "Failed to update seller goal" });
}
});
router.delete("/seller-goals/:id", async (req: Request, res: Response) => {
try {
await db.delete(retailSellerGoals).where(eq(retailSellerGoals.id, parseInt(req.params.id)));
res.json({ success: true });
} catch (error) {
console.error("Error deleting seller goal:", error);
res.status(500).json({ error: "Failed to delete seller goal" });
}
});
// ========== METAS DA LOJA ==========
router.get("/store-goals", async (req: Request, res: Response) => {
try {
const { storeId, month, year } = req.query;
const conditions = [];
if (storeId) conditions.push(eq(retailStoreGoals.storeId, parseInt(storeId as string)));
if (month) conditions.push(eq(retailStoreGoals.month, parseInt(month as string)));
if (year) conditions.push(eq(retailStoreGoals.year, parseInt(year as string)));
const goals = conditions.length > 0
? await db.select().from(retailStoreGoals).where(and(...conditions))
: await db.select().from(retailStoreGoals);
res.json(goals);
} catch (error) {
console.error("Error fetching store goals:", error);
res.status(500).json({ error: "Failed to fetch store goals" });
}
});
router.post("/store-goals", async (req: Request, res: Response) => {
try {
const [goal] = await db.insert(retailStoreGoals).values(req.body).returning();
res.status(201).json(goal);
} catch (error) {
console.error("Error creating store goal:", error);
res.status(500).json({ error: "Failed to create store goal" });
}
});
router.put("/store-goals/:id", async (req: Request, res: Response) => {
try {
const [goal] = await db.update(retailStoreGoals)
.set({ ...req.body, updatedAt: sql`CURRENT_TIMESTAMP` })
.where(eq(retailStoreGoals.id, parseInt(req.params.id)))
.returning();
res.json(goal);
} catch (error) {
console.error("Error updating store goal:", error);
res.status(500).json({ error: "Failed to update store goal" });
}
});
// ========== FECHAMENTO DE COMISSÃO ==========
router.get("/commission-closures", async (req: Request, res: Response) => {
try {
const { sellerId, storeId, status, periodType } = req.query;
const conditions = [];
if (sellerId) conditions.push(eq(retailCommissionClosures.sellerId, parseInt(sellerId as string)));
if (storeId) conditions.push(eq(retailCommissionClosures.storeId, parseInt(storeId as string)));
if (status) conditions.push(eq(retailCommissionClosures.status, status as string));
if (periodType) conditions.push(eq(retailCommissionClosures.periodType, periodType as string));
const closures = conditions.length > 0
? await db.select().from(retailCommissionClosures).where(and(...conditions)).orderBy(desc(retailCommissionClosures.createdAt))
: await db.select().from(retailCommissionClosures).orderBy(desc(retailCommissionClosures.createdAt));
res.json(closures);
} catch (error) {
console.error("Error fetching commission closures:", error);
res.status(500).json({ error: "Failed to fetch commission closures" });
}
});
router.post("/commission-closures", async (req: Request, res: Response) => {
try {
const [closure] = await db.insert(retailCommissionClosures).values(req.body).returning();
res.status(201).json(closure);
} catch (error) {
console.error("Error creating commission closure:", error);
res.status(500).json({ error: "Failed to create commission closure" });
}
});
router.put("/commission-closures/:id", async (req: Request, res: Response) => {
try {
const [closure] = await db.update(retailCommissionClosures)
.set({ ...req.body, updatedAt: sql`CURRENT_TIMESTAMP` })
.where(eq(retailCommissionClosures.id, parseInt(req.params.id)))
.returning();
res.json(closure);
} catch (error) {
console.error("Error updating commission closure:", error);
res.status(500).json({ error: "Failed to update commission closure" });
}
});
// Calcular comissões para período
router.post("/commission-closures/calculate", async (req: Request, res: Response) => {
try {
const { sellerId, storeId, periodType, periodStart, periodEnd, commissionRate } = req.body;
// Buscar vendas do período
const startDate = new Date(periodStart);
const endDate = new Date(periodEnd);
endDate.setHours(23, 59, 59, 999);
const salesConditions = [
gte(posSales.createdAt, startDate),
lte(posSales.createdAt, endDate),
eq(posSales.status, "completed")
];
// Se sellerId, buscar vendas do vendedor específico
let sellerName: string | undefined;
if (sellerId) {
const [seller] = await db.select().from(retailSellers).where(eq(retailSellers.id, sellerId));
if (seller) {
sellerName = seller.name;
salesConditions.push(eq(posSales.soldBy, seller.name));
}
}
const sales = await db.select().from(posSales).where(and(...salesConditions));
// Calcular total de vendas
const totalSales = sales.reduce((sum, s) => sum + parseFloat(s.totalAmount || "0"), 0);
const salesCount = sales.length;
// Buscar devoluções do MÊS CORRENTE (independente da data da venda original)
const currentMonthStart = new Date();
currentMonthStart.setDate(1);
currentMonthStart.setHours(0, 0, 0, 0);
const currentMonthEnd = new Date(currentMonthStart.getFullYear(), currentMonthStart.getMonth() + 1, 0);
currentMonthEnd.setHours(23, 59, 59, 999);
const returnConditions = [
gte(returnExchanges.createdAt, currentMonthStart),
lte(returnExchanges.createdAt, currentMonthEnd),
eq(returnExchanges.returnType, "return"),
eq(returnExchanges.status, "approved")
];
const returns = await db.select().from(returnExchanges).where(and(...returnConditions));
// Filtrar devoluções do vendedor se necessário (precisa cruzar com vendas originais)
let totalReturns = 0;
let returnsCount = 0;
for (const ret of returns) {
if (ret.originalSaleId) {
const [originalSale] = await db.select().from(posSales).where(eq(posSales.id, ret.originalSaleId));
if (originalSale) {
if (!sellerName || originalSale.soldBy === sellerName) {
totalReturns += parseFloat(ret.refundAmount || "0");
returnsCount++;
}
}
}
}
// Calcular comissão
const netSales = totalSales - totalReturns;
const rate = parseFloat(commissionRate || "0") / 100;
const commissionAmount = netSales * rate;
// Verificar meta para bônus
let bonusAmount = 0;
if (sellerId) {
const currentMonth = new Date().getMonth() + 1;
const currentYear = new Date().getFullYear();
const [goal] = await db.select().from(retailSellerGoals)
.where(and(
eq(retailSellerGoals.sellerId, sellerId),
eq(retailSellerGoals.month, currentMonth),
eq(retailSellerGoals.year, currentYear)
));
if (goal && netSales >= parseFloat(goal.goalAmount)) {
bonusAmount = parseFloat(goal.bonus || "0");
}
}
res.json({
sellerId,
sellerName,
periodType,
periodStart,
periodEnd,
totalSales: totalSales.toFixed(2),
totalReturns: totalReturns.toFixed(2),
netSales: netSales.toFixed(2),
commissionRate: commissionRate,
commissionAmount: commissionAmount.toFixed(2),
bonusAmount: bonusAmount.toFixed(2),
totalAmount: (commissionAmount + bonusAmount).toFixed(2),
salesCount,
returnsCount,
salesDetails: sales.map(s => ({ id: s.id, saleNumber: s.saleNumber, total: s.totalAmount, date: s.createdAt })),
returnsDeducted: totalReturns > 0
});
} catch (error) {
console.error("Error calculating commission:", error);
res.status(500).json({ error: "Failed to calculate commission" });
}
});
// Dashboard de vendas por vendedor
router.get("/commission-dashboard", async (req: Request, res: Response) => {
try {
const { month, year, storeId } = req.query;
const currentMonth = month ? parseInt(month as string) : new Date().getMonth() + 1;
const currentYear = year ? parseInt(year as string) : new Date().getFullYear();
// Calcular datas do período
const periodStart = new Date(currentYear, currentMonth - 1, 1);
const periodEnd = new Date(currentYear, currentMonth, 0);
periodEnd.setHours(23, 59, 59, 999);
// Buscar todos os vendedores ativos
const sellers = await db.select().from(retailSellers).where(eq(retailSellers.isActive, true));
// Buscar meta da loja
const storeGoalConditions = [
eq(retailStoreGoals.month, currentMonth),
eq(retailStoreGoals.year, currentYear)
];
if (storeId) storeGoalConditions.push(eq(retailStoreGoals.storeId, parseInt(storeId as string)));
const [storeGoal] = await db.select().from(retailStoreGoals).where(and(...storeGoalConditions));
// Buscar todas as vendas do período
const sales = await db.select().from(posSales)
.where(and(
gte(posSales.createdAt, periodStart),
lte(posSales.createdAt, periodEnd),
eq(posSales.status, "completed")
));
// Buscar devoluções do mês
const returns = await db.select().from(returnExchanges)
.where(and(
gte(returnExchanges.createdAt, periodStart),
lte(returnExchanges.createdAt, periodEnd),
eq(returnExchanges.returnType, "return"),
eq(returnExchanges.status, "approved")
));
// Agrupar por vendedor
const sellerStats = await Promise.all(sellers.map(async (seller) => {
const sellerSales = sales.filter(s => s.soldBy === seller.name);
const totalSales = sellerSales.reduce((sum, s) => sum + parseFloat(s.totalAmount || "0"), 0);
// Calcular devoluções do vendedor
let totalReturns = 0;
for (const ret of returns) {
if (ret.originalSaleId) {
const [originalSale] = await db.select().from(posSales).where(eq(posSales.id, ret.originalSaleId));
if (originalSale && originalSale.soldBy === seller.name) {
totalReturns += parseFloat(ret.refundAmount || "0");
}
}
}
const netSales = totalSales - totalReturns;
// Buscar meta do vendedor
const [sellerGoal] = await db.select().from(retailSellerGoals)
.where(and(
eq(retailSellerGoals.sellerId, seller.id),
eq(retailSellerGoals.month, currentMonth),
eq(retailSellerGoals.year, currentYear)
));
// Buscar plano de comissão
let commissionRate = 0;
if (seller.commissionPlanId) {
const [plan] = await db.select().from(retailCommissionPlans).where(eq(retailCommissionPlans.id, seller.commissionPlanId));
if (plan) {
commissionRate = parseFloat(plan.basePercent || "0");
}
}
const commissionAmount = netSales * (commissionRate / 100);
const goalAmount = sellerGoal ? parseFloat(sellerGoal.goalAmount) : 0;
const goalPercent = goalAmount > 0 ? (netSales / goalAmount) * 100 : 0;
const metGoal = goalPercent >= 100;
const bonus = metGoal && sellerGoal ? parseFloat(sellerGoal.bonus || "0") : 0;
return {
sellerId: seller.id,
sellerName: seller.name,
salesCount: sellerSales.length,
totalSales,
totalReturns,
netSales,
goalAmount,
goalPercent: goalPercent.toFixed(1),
metGoal,
commissionRate,
commissionAmount,
bonus,
totalCommission: commissionAmount + bonus
};
}));
// Totais gerais
const totalStoreSales = sales.reduce((sum, s) => sum + parseFloat(s.totalAmount || "0"), 0);
const totalStoreReturns = returns.reduce((sum, r) => sum + parseFloat(r.refundAmount || "0"), 0);
const netStoreSales = totalStoreSales - totalStoreReturns;
const storeGoalAmount = storeGoal ? parseFloat(storeGoal.goalAmount) : 0;
const storeGoalPercent = storeGoalAmount > 0 ? (netStoreSales / storeGoalAmount) * 100 : 0;
res.json({
period: { month: currentMonth, year: currentYear },
store: {
totalSales: totalStoreSales,
totalReturns: totalStoreReturns,
netSales: netStoreSales,
goalAmount: storeGoalAmount,
goalPercent: storeGoalPercent.toFixed(1),
metGoal: storeGoalPercent >= 100,
salesCount: sales.length,
returnsCount: returns.length
},
sellers: sellerStats.sort((a, b) => b.netSales - a.netSales)
});
} catch (error) {
console.error("Error fetching commission dashboard:", error);
res.status(500).json({ error: "Failed to fetch commission dashboard" });
}
});
// ========== PEDIDOS DE COMPRA ==========
// Listar pedidos de compra
router.get("/purchase-orders", async (req: Request, res: Response) => {
try {
const { status, supplierId, startDate, endDate, limit: queryLimit } = req.query;
const limitNum = parseInt(queryLimit as string) || 100;
let conditions: any[] = [];
if (status) {
conditions.push(eq(purchaseOrders.status, status as string));
}
if (supplierId) {
conditions.push(eq(purchaseOrders.supplierId, parseInt(supplierId as string)));
}
if (startDate) {
conditions.push(gte(purchaseOrders.orderDate, new Date(startDate as string)));
}
if (endDate) {
conditions.push(lte(purchaseOrders.orderDate, new Date(endDate as string)));
}
const orders = conditions.length > 0
? await db.select().from(purchaseOrders).where(and(...conditions)).orderBy(desc(purchaseOrders.orderDate)).limit(limitNum)
: await db.select().from(purchaseOrders).orderBy(desc(purchaseOrders.orderDate)).limit(limitNum);
// Buscar itens e fornecedor para cada pedido
const ordersWithDetails = await Promise.all(orders.map(async (order) => {
const items = await db.select().from(purchaseOrderItems).where(eq(purchaseOrderItems.orderId, order.id));
let supplier = null;
if (order.supplierId) {
const [s] = await db.select().from(suppliers).where(eq(suppliers.id, order.supplierId));
supplier = s;
}
return { ...order, items, supplier };
}));
res.json(ordersWithDetails);
} catch (error) {
console.error("Error fetching purchase orders:", error);
res.status(500).json({ error: "Failed to fetch purchase orders" });
}
});
// Buscar pedido de compra por ID
router.get("/purchase-orders/:id", async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
const [order] = await db.select().from(purchaseOrders).where(eq(purchaseOrders.id, id));
if (!order) {
return res.status(404).json({ error: "Purchase order not found" });
}
const items = await db.select().from(purchaseOrderItems).where(eq(purchaseOrderItems.orderId, id));
let supplier = null;
if (order.supplierId) {
const [s] = await db.select().from(suppliers).where(eq(suppliers.id, order.supplierId));
supplier = s;
}
res.json({ ...order, items, supplier });
} catch (error) {
console.error("Error fetching purchase order:", error);
res.status(500).json({ error: "Failed to fetch purchase order" });
}
});
// Criar pedido de compra
router.post("/purchase-orders", async (req: Request, res: Response) => {
try {
const { items, warehouseId, ...orderData } = req.body;
const user = (req as any).user;
// Gerar número do pedido
const [lastOrder] = await db.select({ orderNumber: purchaseOrders.orderNumber })
.from(purchaseOrders)
.orderBy(desc(purchaseOrders.id))
.limit(1);
const lastNum = lastOrder ? parseInt(lastOrder.orderNumber.replace("PC", "")) || 0 : 0;
const orderNumber = `PC${String(lastNum + 1).padStart(6, "0")}`;
// Calcular total
const total = items.reduce((sum: number, item: any) => {
return sum + (parseFloat(item.quantity) * parseFloat(item.unitPrice));
}, 0);
// Criar pedido
const [newOrder] = await db.insert(purchaseOrders).values({
...orderData,
orderNumber,
total: total.toString(),
status: orderData.status || "draft"
}).returning();
// Criar itens
for (const item of items) {
const itemTotal = parseFloat(item.quantity) * parseFloat(item.unitPrice);
await db.insert(purchaseOrderItems).values({
orderId: newOrder.id,
productId: item.productId || null,
productName: item.productName,
quantity: item.quantity.toString(),
unitPrice: item.unitPrice.toString(),
total: itemTotal.toString()
});
}
// Log activity
await logActivity({
activityType: "purchase_order_created",
entityType: "purchase_order",
entityId: newOrder.id,
title: `Pedido de compra ${orderNumber} criado`,
description: `Valor total: R$ ${total.toFixed(2)}`,
createdBy: user?.id,
createdByName: user?.name || user?.username,
metadata: { orderNumber, total, itemCount: items.length, warehouseId }
});
res.status(201).json(newOrder);
} catch (error) {
console.error("Error creating purchase order:", error);
res.status(500).json({ error: "Failed to create purchase order" });
}
});
// Atualizar status do pedido de compra
router.patch("/purchase-orders/:id/status", async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
const { status } = req.body;
const user = (req as any).user;
const [order] = await db.select().from(purchaseOrders).where(eq(purchaseOrders.id, id));
if (!order) {
return res.status(404).json({ error: "Purchase order not found" });
}
const [updated] = await db.update(purchaseOrders)
.set({ status, updatedAt: new Date() })
.where(eq(purchaseOrders.id, id))
.returning();
await logActivity({
activityType: "purchase_order_status_changed",
entityType: "purchase_order",
entityId: id,
title: `Pedido ${order.orderNumber} - Status alterado para ${status}`,
createdBy: user?.id,
createdByName: user?.name || user?.username
});
res.json(updated);
} catch (error) {
console.error("Error updating purchase order status:", error);
res.status(500).json({ error: "Failed to update purchase order status" });
}
});
// Receber pedido de compra (entrada no estoque)
router.post("/purchase-orders/:id/receive", async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
const { warehouseId, serials } = req.body; // serials: { itemId: ["serial1", "serial2"] }
const user = (req as any).user;
const [order] = await db.select().from(purchaseOrders).where(eq(purchaseOrders.id, id));
if (!order) {
return res.status(404).json({ error: "Purchase order not found" });
}
if (order.status === "received") {
return res.status(400).json({ error: "Order already received" });
}
const items = await db.select().from(purchaseOrderItems).where(eq(purchaseOrderItems.orderId, id));
// Processar entrada de cada item
for (const item of items) {
const quantity = parseFloat(item.quantity);
// Atualizar estoque do depósito
if (item.productId) {
const [existingStock] = await db.select().from(retailWarehouseStock)
.where(and(
eq(retailWarehouseStock.warehouseId, warehouseId),
eq(retailWarehouseStock.productId, item.productId)
));
if (existingStock) {
await db.update(retailWarehouseStock)
.set({
quantity: (parseFloat(existingStock.quantity) + quantity).toString(),
updatedAt: new Date()
})
.where(eq(retailWarehouseStock.id, existingStock.id));
} else {
await db.insert(retailWarehouseStock).values({
warehouseId,
productId: item.productId,
quantity: quantity.toString()
});
}
// Criar movimentação
await db.insert(retailStockMovements).values({
warehouseId,
productId: item.productId,
movementType: "entry",
operationType: "purchase",
quantity: quantity.toString(),
unitCost: item.unitPrice,
totalCost: item.total,
referenceType: "purchase_order",
referenceId: order.id,
notes: `Pedido ${order.orderNumber}`,
userId: user?.id
});
}
// Registrar seriais/IMEIs se fornecidos
const itemSerials = serials?.[item.id] || [];
for (const serial of itemSerials) {
if (serial && serial.trim()) {
await db.insert(retailProductSerials).values({
warehouseId,
productId: item.productId || 0,
serialNumber: serial.trim(),
status: "in_stock",
acquisitionCost: item.unitPrice
});
}
}
}
// Atualizar status do pedido
const [updated] = await db.update(purchaseOrders)
.set({ status: "received", updatedAt: new Date() })
.where(eq(purchaseOrders.id, id))
.returning();
await logActivity({
activityType: "purchase_order_received",
entityType: "purchase_order",
entityId: id,
title: `Pedido ${order.orderNumber} recebido`,
description: `${items.length} itens entrada no depósito`,
createdBy: user?.id,
createdByName: user?.name || user?.username,
metadata: { warehouseId, itemCount: items.length }
});
res.json({ success: true, order: updated });
} catch (error) {
console.error("Error receiving purchase order:", error);
res.status(500).json({ error: "Failed to receive purchase order" });
}
});
// Excluir pedido de compra (apenas rascunhos)
router.delete("/purchase-orders/:id", async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
const user = (req as any).user;
const [order] = await db.select().from(purchaseOrders).where(eq(purchaseOrders.id, id));
if (!order) {
return res.status(404).json({ error: "Purchase order not found" });
}
if (order.status !== "draft") {
return res.status(400).json({ error: "Only draft orders can be deleted" });
}
// Excluir itens
await db.delete(purchaseOrderItems).where(eq(purchaseOrderItems.orderId, id));
// Excluir pedido
await db.delete(purchaseOrders).where(eq(purchaseOrders.id, id));
await logActivity({
activityType: "purchase_order_deleted",
entityType: "purchase_order",
entityId: id,
title: `Pedido ${order.orderNumber} excluído`,
createdBy: user?.id,
createdByName: user?.name || user?.username
});
res.json({ success: true });
} catch (error) {
console.error("Error deleting purchase order:", error);
res.status(500).json({ error: "Failed to delete purchase order" });
}
});
// ========== DEVOLUÇÕES E CRÉDITOS ==========
// Listar devoluções
router.get("/returns", async (req: Request, res: Response) => {
try {
const { status, customerId, limit: queryLimit } = req.query;
const limitNum = parseInt(queryLimit as string) || 100;
let conditions: any[] = [];
if (status) conditions.push(eq(returnExchanges.status, status as string));
if (customerId) conditions.push(eq(returnExchanges.customerId, customerId as string));
const returns = conditions.length > 0
? await db.select().from(returnExchanges).where(and(...conditions)).orderBy(desc(returnExchanges.createdAt)).limit(limitNum)
: await db.select().from(returnExchanges).orderBy(desc(returnExchanges.createdAt)).limit(limitNum);
// Buscar itens de cada devolução
const returnsWithItems = await Promise.all(returns.map(async (ret) => {
const items = await db.select().from(returnExchangeItems).where(eq(returnExchangeItems.returnId, ret.id));
return { ...ret, items };
}));
res.json(returnsWithItems);
} catch (error) {
console.error("Error fetching returns:", error);
res.status(500).json({ error: "Failed to fetch returns" });
}
});
// Criar devolução com geração de crédito
router.post("/returns", async (req: Request, res: Response) => {
try {
const { items, generateCredit, creditExpirationDays, ...returnData } = req.body;
const user = (req as any).user;
// Gerar número da devolução
const [lastReturn] = await db.select({ returnNumber: returnExchanges.returnNumber })
.from(returnExchanges)
.orderBy(desc(returnExchanges.id))
.limit(1);
const lastNum = lastReturn ? parseInt(lastReturn.returnNumber.replace("DEV", "")) || 0 : 0;
const returnNumber = `DEV${String(lastNum + 1).padStart(6, "0")}`;
// Criar devolução
const [newReturn] = await db.insert(returnExchanges).values({
...returnData,
returnNumber,
processedBy: user?.name || user?.username,
status: "approved"
}).returning();
// Criar itens da devolução
let totalRefund = 0;
for (const item of items || []) {
const itemTotal = parseFloat(item.quantity || 1) * parseFloat(item.unitPrice || "0");
totalRefund += itemTotal;
await db.insert(returnExchangeItems).values({
returnId: newReturn.id,
itemCode: item.itemCode,
itemName: item.itemName,
quantity: parseInt(item.quantity) || 1,
imei: item.imei,
deviceId: item.deviceId,
reason: item.reason,
refundAmount: itemTotal.toString()
});
}
// Atualizar valor total da devolução
await db.update(returnExchanges)
.set({ refundAmount: totalRefund.toString() })
.where(eq(returnExchanges.id, newReturn.id));
// Gerar crédito para o cliente se solicitado
let credit = null;
if (generateCredit && returnData.customerId && totalRefund > 0) {
// Buscar dados do cliente
const [customer] = await db.select().from(persons).where(eq(persons.id, parseInt(returnData.customerId)));
const expiresAt = creditExpirationDays
? new Date(Date.now() + creditExpirationDays * 24 * 60 * 60 * 1000)
: null;
const [newCredit] = await db.insert(customerCredits).values({
personId: parseInt(returnData.customerId),
customerName: customer?.fullName || returnData.customerName || "Cliente",
customerCpf: customer?.cpfCnpj || null,
amount: totalRefund.toString(),
usedAmount: "0",
remainingAmount: totalRefund.toString(),
origin: "refund",
originId: newReturn.id,
description: `Crédito de devolução ${returnNumber}`,
expiresAt,
status: "active",
createdBy: user?.name || user?.username
}).returning();
credit = newCredit;
}
await logActivity({
activityType: "return_created",
entityType: "return",
entityId: newReturn.id,
title: `Devolução ${returnNumber} registrada`,
description: `Valor: R$ ${totalRefund.toFixed(2)}${credit ? " - Crédito gerado" : ""}`,
createdBy: user?.id,
createdByName: user?.name || user?.username
});
res.status(201).json({ return: { ...newReturn, refundAmount: totalRefund }, credit });
} catch (error) {
console.error("Error creating return:", error);
res.status(500).json({ error: "Failed to create return" });
}
});
// Listar créditos de um cliente
router.get("/customer-credits/:personId", async (req: Request, res: Response) => {
try {
const personId = parseInt(req.params.personId);
const credits = await db.select().from(customerCredits)
.where(eq(customerCredits.personId, personId))
.orderBy(desc(customerCredits.createdAt));
// Calcular saldo total disponível
const activeCredits = credits.filter(c => c.status === "active");
const totalAvailable = activeCredits.reduce((sum, c) => sum + parseFloat(c.remainingAmount || "0"), 0);
res.json({ credits, totalAvailable });
} catch (error) {
console.error("Error fetching customer credits:", error);
res.status(500).json({ error: "Failed to fetch customer credits" });
}
});
// Obter comprovante de crédito
router.get("/customer-credits/:creditId/receipt", async (req: Request, res: Response) => {
try {
const creditId = parseInt(req.params.creditId);
const [credit] = await db.select().from(customerCredits).where(eq(customerCredits.id, creditId));
if (!credit) {
return res.status(404).json({ error: "Credit not found" });
}
// Buscar dados do cliente
const [customer] = await db.select().from(persons).where(eq(persons.id, credit.personId));
// Buscar dados da origem (se for devolução)
let originData = null;
if (credit.origin === "refund" && credit.originId) {
const [returnData] = await db.select().from(returnExchanges).where(eq(returnExchanges.id, credit.originId));
originData = returnData;
}
res.json({
credit,
customer,
originData,
receiptData: {
title: "COMPROVANTE DE CRÉDITO",
creditNumber: `CR${String(credit.id).padStart(6, "0")}`,
customerName: credit.customerName,
customerCpf: credit.customerCpf,
amount: credit.amount,
remainingAmount: credit.remainingAmount,
origin: credit.origin === "refund" ? "Devolução" :
credit.origin === "trade_in" ? "Trade-In" :
credit.origin === "bonus" ? "Bonificação" : "Promoção",
originNumber: originData?.returnNumber || null,
createdAt: credit.createdAt,
expiresAt: credit.expiresAt,
status: credit.status
}
});
} catch (error) {
console.error("Error fetching credit receipt:", error);
res.status(500).json({ error: "Failed to fetch credit receipt" });
}
});
// Usar crédito em uma venda
router.post("/customer-credits/:creditId/use", async (req: Request, res: Response) => {
try {
const creditId = parseInt(req.params.creditId);
const { amount, saleId } = req.body;
const user = (req as any).user;
const [credit] = await db.select().from(customerCredits).where(eq(customerCredits.id, creditId));
if (!credit) {
return res.status(404).json({ error: "Credit not found" });
}
if (credit.status !== "active") {
return res.status(400).json({ error: "Credit is not active" });
}
const remaining = parseFloat(credit.remainingAmount || "0");
const useAmount = Math.min(parseFloat(amount), remaining);
if (useAmount <= 0) {
return res.status(400).json({ error: "Invalid amount" });
}
const newRemaining = remaining - useAmount;
const newUsed = parseFloat(credit.usedAmount || "0") + useAmount;
const newStatus = newRemaining <= 0 ? "used" : "active";
const [updated] = await db.update(customerCredits)
.set({
usedAmount: newUsed.toString(),
remainingAmount: newRemaining.toString(),
status: newStatus,
usedInSaleId: saleId || null,
updatedAt: new Date()
})
.where(eq(customerCredits.id, creditId))
.returning();
await logActivity({
activityType: "credit_used",
entityType: "customer_credit",
entityId: creditId,
title: `Crédito utilizado: R$ ${useAmount.toFixed(2)}`,
description: `Cliente: ${credit.customerName}`,
createdBy: user?.id,
createdByName: user?.name || user?.username
});
res.json({ credit: updated, amountUsed: useAmount });
} catch (error) {
console.error("Error using credit:", error);
res.status(500).json({ error: "Failed to use credit" });
}
});
// ========== PLUS ERP SYNC ROUTES ==========
const plusSync = retailPlusSyncService;
router.get("/plus/status", async (req: Request, res: Response) => {
try {
const status = await plusSync.getPlusStatus();
res.json(status);
} catch (error) {
console.error("Error checking Plus status:", error);
res.status(500).json({ error: "Failed to check Plus connection" });
}
});
router.post("/plus/sync/customers", requireModule("plus"), async (req: Request, res: Response) => {
try {
const { tenantId, empresaId } = req.body;
if (!tenantId) return res.status(400).json({ error: "tenantId required" });
const result = await plusSync.syncAllPersonsToPlus(tenantId, empresaId);
res.json(result);
} catch (error) {
console.error("Error syncing customers to Plus:", error);
res.status(500).json({ error: "Failed to sync customers" });
}
});
router.post("/plus/sync/sales", requireModule("plus"), async (req: Request, res: Response) => {
try {
const { saleId } = req.body;
if (!saleId) return res.status(400).json({ error: "saleId required" });
const result = await plusSync.syncSaleToPlus(saleId);
res.json(result);
} catch (error) {
console.error("Error syncing sale to Plus:", error);
res.status(500).json({ error: "Failed to sync sale" });
}
});
router.post("/plus/sync/nfe", requireModule("plus"), async (req: Request, res: Response) => {
try {
const { saleId, tipo } = req.body;
if (!saleId) return res.status(400).json({ error: "saleId required" });
const result = await plusSync.emitirNFeSale(saleId, tipo || "nfce");
res.json(result);
} catch (error) {
console.error("Error emitting NF-e:", error);
res.status(500).json({ error: "Failed to emit NF-e" });
}
});
router.post("/plus/import/customers", requireModule("plus"), async (req: Request, res: Response) => {
try {
const { tenantId, empresaId } = req.body;
if (!tenantId) return res.status(400).json({ error: "tenantId required" });
const result = await plusSync.importClientesFromPlus(tenantId, empresaId);
res.json(result);
} catch (error) {
console.error("Error importing customers from Plus:", error);
res.status(500).json({ error: "Failed to import customers" });
}
});
router.post("/plus/import/products", requireModule("plus"), async (req: Request, res: Response) => {
try {
const { tenantId, empresaId } = req.body;
if (!tenantId) return res.status(400).json({ error: "tenantId required" });
const result = await plusSync.importProdutosFromPlus(tenantId, empresaId);
res.json(result);
} catch (error) {
console.error("Error importing products from Plus:", error);
res.status(500).json({ error: "Failed to import products" });
}
});
router.get("/plus/empresas", async (req: Request, res: Response) => {
try {
const tenantId = parseInt(req.query.tenantId as string) || 1;
const empresas = await db.select().from(tenantEmpresas)
.where(and(eq(tenantEmpresas.tenantId, tenantId), eq(tenantEmpresas.status, "active")));
res.json(empresas);
} catch (error) {
console.error("Error fetching empresas:", error);
res.status(500).json({ error: "Failed to fetch empresas" });
}
});
router.post("/plus/empresas", async (req: Request, res: Response) => {
try {
const tenantId = parseInt(req.body.tenantId as string) || 1;
const { razaoSocial, nomeFantasia, cnpj, tipo } = req.body;
if (!razaoSocial || !cnpj) return res.status(400).json({ error: "razaoSocial and cnpj required" });
const [empresa] = await db.insert(tenantEmpresas).values({
tenantId,
razaoSocial,
nomeFantasia: nomeFantasia || razaoSocial,
cnpj,
tipo: tipo || "matriz",
status: "active",
}).returning();
res.json(empresa);
} catch (error) {
console.error("Error creating empresa:", error);
res.status(500).json({ error: "Failed to create empresa" });
}
});
router.post("/plus/empresas/:id/bind", async (req: Request, res: Response) => {
try {
const empresaLocalId = parseInt(req.params.id);
const { plusEmpresaId } = req.body;
if (!plusEmpresaId) return res.status(400).json({ error: "plusEmpresaId required" });
const [updated] = await db.update(tenantEmpresas)
.set({ plusEmpresaId })
.where(eq(tenantEmpresas.id, empresaLocalId))
.returning();
res.json(updated);
} catch (error) {
console.error("Error binding empresa to Plus:", error);
res.status(500).json({ error: "Failed to bind empresa" });
}
});
export default router;