[MCP] feat: Connect MCP tools to actual services
- orders-tools.service.ts → ServiceOrderService (6 tools) - create_service_order, get_service_order, list_service_orders - update_order_status, get_orders_kanban, get_orders_dashboard - inventory-tools.service.ts → PartService (8 tools) - search_parts, get_part_details, check_stock - get_low_stock_parts, adjust_stock, get_inventory_value - create_part, list_parts - customers-tools.service.ts → CustomersService (6 tools) - search_customers, get_customer, create_customer - update_customer, list_customers, get_customer_stats Removed mock data, now using real database operations. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
669c0c2911
commit
d5f1453492
@ -1,51 +1,138 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import {
|
||||
McpToolProvider,
|
||||
McpToolDefinition,
|
||||
McpToolHandler,
|
||||
McpContext,
|
||||
} from '../interfaces';
|
||||
import { CustomersService } from '../../customers/services/customers.service';
|
||||
import { CreateCustomerDto, CustomerFilters } from '../../customers/customers.dto';
|
||||
import { CustomerType } from '../../customers/entities/customer.entity';
|
||||
|
||||
/**
|
||||
* Customers Tools Service
|
||||
* Provides MCP tools for customer management.
|
||||
*
|
||||
* TODO: Connect to actual CustomersService when available.
|
||||
* Connected to actual CustomersService for real data operations.
|
||||
*/
|
||||
export class CustomersToolsService implements McpToolProvider {
|
||||
private customersService: CustomersService;
|
||||
|
||||
constructor(dataSource: DataSource) {
|
||||
this.customersService = new CustomersService(dataSource);
|
||||
}
|
||||
|
||||
getTools(): McpToolDefinition[] {
|
||||
return [
|
||||
{
|
||||
name: 'search_customers',
|
||||
description: 'Busca clientes por nombre, telefono o email',
|
||||
description: 'Busca clientes por nombre, teléfono, email o RFC',
|
||||
category: 'customers',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', description: 'Texto de busqueda' },
|
||||
limit: { type: 'number', description: 'Limite de resultados', default: 10 },
|
||||
query: { type: 'string', description: 'Término de búsqueda' },
|
||||
limit: { type: 'number', default: 10, description: 'Máximo de resultados' },
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
returns: { type: 'array' },
|
||||
returns: { type: 'array', description: 'Lista de clientes encontrados' },
|
||||
},
|
||||
{
|
||||
name: 'get_customer_balance',
|
||||
description: 'Obtiene el saldo actual de un cliente',
|
||||
name: 'get_customer',
|
||||
description: 'Obtiene información detallada de un cliente',
|
||||
category: 'customers',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customer_id: { type: 'string', format: 'uuid' },
|
||||
customer_id: { type: 'string', format: 'uuid', description: 'ID del cliente' },
|
||||
email: { type: 'string', description: 'Email del cliente' },
|
||||
phone: { type: 'string', description: 'Teléfono del cliente' },
|
||||
rfc: { type: 'string', description: 'RFC del cliente' },
|
||||
},
|
||||
},
|
||||
returns: { type: 'object', description: 'Información del cliente' },
|
||||
},
|
||||
{
|
||||
name: 'create_customer',
|
||||
description: 'Registra un nuevo cliente',
|
||||
category: 'customers',
|
||||
permissions: ['customers.create'],
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Nombre completo o razón social' },
|
||||
customer_type: {
|
||||
type: 'string',
|
||||
enum: ['individual', 'company', 'fleet', 'government'],
|
||||
default: 'individual',
|
||||
description: 'Tipo de cliente'
|
||||
},
|
||||
email: { type: 'string', format: 'email' },
|
||||
phone: { type: 'string', description: 'Teléfono principal' },
|
||||
rfc: { type: 'string', description: 'RFC (opcional para facturación)' },
|
||||
address: { type: 'string' },
|
||||
city: { type: 'string' },
|
||||
state: { type: 'string' },
|
||||
postal_code: { type: 'string' },
|
||||
credit_days: { type: 'number', default: 0, description: 'Días de crédito' },
|
||||
credit_limit: { type: 'number', default: 0, description: 'Límite de crédito en MXN' },
|
||||
notes: { type: 'string' },
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
returns: { type: 'object', description: 'Cliente creado' },
|
||||
},
|
||||
{
|
||||
name: 'update_customer',
|
||||
description: 'Actualiza información de un cliente',
|
||||
category: 'customers',
|
||||
permissions: ['customers.update'],
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customer_id: { type: 'string', format: 'uuid', description: 'ID del cliente' },
|
||||
name: { type: 'string' },
|
||||
email: { type: 'string' },
|
||||
phone: { type: 'string' },
|
||||
rfc: { type: 'string' },
|
||||
address: { type: 'string' },
|
||||
credit_days: { type: 'number' },
|
||||
credit_limit: { type: 'number' },
|
||||
notes: { type: 'string' },
|
||||
},
|
||||
required: ['customer_id'],
|
||||
},
|
||||
returns: {
|
||||
returns: { type: 'object', description: 'Cliente actualizado' },
|
||||
},
|
||||
{
|
||||
name: 'list_customers',
|
||||
description: 'Lista clientes con filtros opcionales',
|
||||
category: 'customers',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
balance: { type: 'number' },
|
||||
credit_limit: { type: 'number' },
|
||||
customer_type: {
|
||||
type: 'string',
|
||||
enum: ['individual', 'company', 'fleet', 'government'],
|
||||
},
|
||||
search: { type: 'string' },
|
||||
is_active: { type: 'boolean' },
|
||||
has_credit: { type: 'boolean', description: 'Solo clientes con crédito' },
|
||||
page: { type: 'number', default: 1 },
|
||||
limit: { type: 'number', default: 20 },
|
||||
},
|
||||
},
|
||||
returns: { type: 'object', description: 'Lista paginada de clientes' },
|
||||
},
|
||||
{
|
||||
name: 'get_customer_stats',
|
||||
description: 'Obtiene estadísticas generales de clientes',
|
||||
category: 'customers',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
returns: { type: 'object', description: 'Estadísticas de clientes' },
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -53,7 +140,11 @@ export class CustomersToolsService implements McpToolProvider {
|
||||
getHandler(toolName: string): McpToolHandler | undefined {
|
||||
const handlers: Record<string, McpToolHandler> = {
|
||||
search_customers: this.searchCustomers.bind(this),
|
||||
get_customer_balance: this.getCustomerBalance.bind(this),
|
||||
get_customer: this.getCustomer.bind(this),
|
||||
create_customer: this.createCustomer.bind(this),
|
||||
update_customer: this.updateCustomer.bind(this),
|
||||
list_customers: this.listCustomers.bind(this),
|
||||
get_customer_stats: this.getCustomerStats.bind(this),
|
||||
};
|
||||
return handlers[toolName];
|
||||
}
|
||||
@ -61,34 +152,254 @@ export class CustomersToolsService implements McpToolProvider {
|
||||
private async searchCustomers(
|
||||
params: { query: string; limit?: number },
|
||||
context: McpContext
|
||||
): Promise<any[]> {
|
||||
// TODO: Connect to actual customers service
|
||||
return [
|
||||
{
|
||||
id: 'customer-1',
|
||||
name: 'Juan Perez',
|
||||
phone: '+52 55 1234 5678',
|
||||
email: 'juan@example.com',
|
||||
balance: 500.00,
|
||||
credit_limit: 5000.00,
|
||||
message: 'Conectar a CustomersService real',
|
||||
},
|
||||
];
|
||||
): Promise<any> {
|
||||
const customers = await this.customersService.search(
|
||||
context.tenantId,
|
||||
params.query,
|
||||
params.limit || 10
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
customers: customers.map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
customer_type: c.customerType,
|
||||
email: c.email,
|
||||
phone: c.phone,
|
||||
rfc: c.rfc,
|
||||
total_orders: c.totalOrders,
|
||||
total_spent: c.totalSpent,
|
||||
})),
|
||||
count: customers.length,
|
||||
};
|
||||
}
|
||||
|
||||
private async getCustomerBalance(
|
||||
params: { customer_id: string },
|
||||
private async getCustomer(
|
||||
params: { customer_id?: string; email?: string; phone?: string; rfc?: string },
|
||||
context: McpContext
|
||||
): Promise<any> {
|
||||
// TODO: Connect to actual customers service
|
||||
let customer = null;
|
||||
|
||||
if (params.customer_id) {
|
||||
customer = await this.customersService.findById(context.tenantId, params.customer_id);
|
||||
} else if (params.email) {
|
||||
customer = await this.customersService.findByEmail(context.tenantId, params.email);
|
||||
} else if (params.phone) {
|
||||
customer = await this.customersService.findByPhone(context.tenantId, params.phone);
|
||||
} else if (params.rfc) {
|
||||
customer = await this.customersService.findByRfc(context.tenantId, params.rfc);
|
||||
}
|
||||
|
||||
if (!customer) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Cliente no encontrado',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
customer_id: params.customer_id,
|
||||
customer_name: 'Cliente ejemplo',
|
||||
balance: 500.00,
|
||||
credit_limit: 5000.00,
|
||||
available_credit: 4500.00,
|
||||
last_purchase: new Date().toISOString(),
|
||||
message: 'Conectar a CustomersService real',
|
||||
success: true,
|
||||
customer: {
|
||||
id: customer.id,
|
||||
name: customer.name,
|
||||
legal_name: customer.legalName,
|
||||
customer_type: customer.customerType,
|
||||
email: customer.email,
|
||||
phone: customer.phone,
|
||||
phone_secondary: customer.phoneSecondary,
|
||||
rfc: customer.rfc,
|
||||
address: customer.address,
|
||||
city: customer.city,
|
||||
state: customer.state,
|
||||
postal_code: customer.postalCode,
|
||||
credit_days: customer.creditDays,
|
||||
credit_limit: customer.creditLimit,
|
||||
credit_balance: customer.creditBalance,
|
||||
discount_labor_pct: customer.discountLaborPct,
|
||||
discount_parts_pct: customer.discountPartsPct,
|
||||
total_orders: customer.totalOrders,
|
||||
total_spent: customer.totalSpent,
|
||||
last_visit_at: customer.lastVisitAt,
|
||||
preferred_contact: customer.preferredContact,
|
||||
notes: customer.notes,
|
||||
is_active: customer.isActive,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async createCustomer(
|
||||
params: {
|
||||
name: string;
|
||||
customer_type?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
rfc?: string;
|
||||
address?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
postal_code?: string;
|
||||
credit_days?: number;
|
||||
credit_limit?: number;
|
||||
notes?: string;
|
||||
},
|
||||
context: McpContext
|
||||
): Promise<any> {
|
||||
try {
|
||||
const dto: CreateCustomerDto = {
|
||||
name: params.name,
|
||||
customerType: (params.customer_type as CustomerType) || CustomerType.INDIVIDUAL,
|
||||
email: params.email,
|
||||
phone: params.phone,
|
||||
rfc: params.rfc,
|
||||
address: params.address,
|
||||
city: params.city,
|
||||
state: params.state,
|
||||
postalCode: params.postal_code,
|
||||
creditDays: params.credit_days || 0,
|
||||
creditLimit: params.credit_limit || 0,
|
||||
notes: params.notes,
|
||||
};
|
||||
|
||||
const customer = await this.customersService.create(
|
||||
context.tenantId,
|
||||
dto,
|
||||
context.userId
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
customer: {
|
||||
id: customer.id,
|
||||
name: customer.name,
|
||||
customer_type: customer.customerType,
|
||||
email: customer.email,
|
||||
phone: customer.phone,
|
||||
},
|
||||
message: `Cliente ${customer.name} registrado exitosamente`,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async updateCustomer(
|
||||
params: {
|
||||
customer_id: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
rfc?: string;
|
||||
address?: string;
|
||||
credit_days?: number;
|
||||
credit_limit?: number;
|
||||
notes?: string;
|
||||
},
|
||||
context: McpContext
|
||||
): Promise<any> {
|
||||
try {
|
||||
const updateData: any = {};
|
||||
if (params.name) updateData.name = params.name;
|
||||
if (params.email) updateData.email = params.email;
|
||||
if (params.phone) updateData.phone = params.phone;
|
||||
if (params.rfc) updateData.rfc = params.rfc;
|
||||
if (params.address) updateData.address = params.address;
|
||||
if (params.credit_days !== undefined) updateData.creditDays = params.credit_days;
|
||||
if (params.credit_limit !== undefined) updateData.creditLimit = params.credit_limit;
|
||||
if (params.notes) updateData.notes = params.notes;
|
||||
|
||||
const customer = await this.customersService.update(
|
||||
context.tenantId,
|
||||
params.customer_id,
|
||||
updateData
|
||||
);
|
||||
|
||||
if (!customer) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Cliente no encontrado',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
customer: {
|
||||
id: customer.id,
|
||||
name: customer.name,
|
||||
email: customer.email,
|
||||
phone: customer.phone,
|
||||
},
|
||||
message: 'Cliente actualizado exitosamente',
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async listCustomers(
|
||||
params: {
|
||||
customer_type?: string;
|
||||
search?: string;
|
||||
is_active?: boolean;
|
||||
has_credit?: boolean;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
},
|
||||
context: McpContext
|
||||
): Promise<any> {
|
||||
const filters: CustomerFilters = {
|
||||
customerType: params.customer_type as CustomerType,
|
||||
search: params.search,
|
||||
isActive: params.is_active,
|
||||
hasCredit: params.has_credit,
|
||||
page: params.page || 1,
|
||||
limit: params.limit || 20,
|
||||
};
|
||||
|
||||
const result = await this.customersService.findAll(context.tenantId, filters);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
customers: result.data.map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
customer_type: c.customerType,
|
||||
email: c.email,
|
||||
phone: c.phone,
|
||||
total_orders: c.totalOrders,
|
||||
total_spent: c.totalSpent,
|
||||
credit_limit: c.creditLimit,
|
||||
is_active: c.isActive,
|
||||
})),
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
total_pages: Math.ceil(result.total / result.limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async getCustomerStats(
|
||||
params: {},
|
||||
context: McpContext
|
||||
): Promise<any> {
|
||||
const stats = await this.customersService.getStats(context.tenantId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
stats: {
|
||||
total_customers: stats.total,
|
||||
active_customers: stats.active,
|
||||
customers_by_type: stats.byType,
|
||||
customers_with_credit: stats.withCredit,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,60 +1,93 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import {
|
||||
McpToolProvider,
|
||||
McpToolDefinition,
|
||||
McpToolHandler,
|
||||
McpContext,
|
||||
} from '../interfaces';
|
||||
import { PartService, CreatePartDto, StockAdjustmentDto } from '../../parts-management/services/part.service';
|
||||
|
||||
/**
|
||||
* Inventory Tools Service
|
||||
* Provides MCP tools for inventory management.
|
||||
*
|
||||
* TODO: Connect to actual InventoryService when available.
|
||||
* Provides MCP tools for parts/inventory management.
|
||||
* Connected to actual PartService for real data operations.
|
||||
*/
|
||||
export class InventoryToolsService implements McpToolProvider {
|
||||
private partService: PartService;
|
||||
|
||||
constructor(dataSource: DataSource) {
|
||||
this.partService = new PartService(dataSource);
|
||||
}
|
||||
|
||||
getTools(): McpToolDefinition[] {
|
||||
return [
|
||||
{
|
||||
name: 'search_parts',
|
||||
description: 'Busca refacciones por SKU, nombre o código de barras',
|
||||
category: 'inventory',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', description: 'Término de búsqueda (SKU, nombre, código de barras)' },
|
||||
limit: { type: 'number', default: 10, description: 'Máximo de resultados' },
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
returns: { type: 'array', description: 'Lista de refacciones encontradas' },
|
||||
},
|
||||
{
|
||||
name: 'get_part_details',
|
||||
description: 'Obtiene detalles completos de una refacción',
|
||||
category: 'inventory',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
part_id: { type: 'string', format: 'uuid', description: 'ID de la refacción' },
|
||||
sku: { type: 'string', description: 'SKU de la refacción' },
|
||||
barcode: { type: 'string', description: 'Código de barras' },
|
||||
},
|
||||
},
|
||||
returns: { type: 'object', description: 'Detalles de la refacción' },
|
||||
},
|
||||
{
|
||||
name: 'check_stock',
|
||||
description: 'Consulta el stock actual de productos',
|
||||
description: 'Consulta el stock actual de refacciones',
|
||||
category: 'inventory',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
product_ids: { type: 'array', description: 'IDs de productos a consultar' },
|
||||
warehouse_id: { type: 'string', description: 'ID del almacen' },
|
||||
part_ids: { type: 'array', items: { type: 'string' }, description: 'IDs de refacciones a consultar' },
|
||||
},
|
||||
required: ['part_ids'],
|
||||
},
|
||||
returns: { type: 'array' },
|
||||
returns: { type: 'array', description: 'Stock de cada refacción' },
|
||||
},
|
||||
{
|
||||
name: 'get_low_stock_products',
|
||||
description: 'Lista productos que estan por debajo del minimo de stock',
|
||||
name: 'get_low_stock_parts',
|
||||
description: 'Lista refacciones con stock bajo (por debajo del mínimo configurado)',
|
||||
category: 'inventory',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
threshold: { type: 'number', description: 'Umbral de stock bajo' },
|
||||
},
|
||||
properties: {},
|
||||
},
|
||||
returns: { type: 'array' },
|
||||
returns: { type: 'array', description: 'Refacciones con stock bajo' },
|
||||
},
|
||||
{
|
||||
name: 'record_inventory_movement',
|
||||
description: 'Registra un movimiento de inventario (entrada, salida, ajuste)',
|
||||
name: 'adjust_stock',
|
||||
description: 'Ajusta el stock de una refacción (entrada, salida o ajuste)',
|
||||
category: 'inventory',
|
||||
permissions: ['inventory.write'],
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
product_id: { type: 'string', format: 'uuid' },
|
||||
quantity: { type: 'number' },
|
||||
movement_type: { type: 'string', enum: ['in', 'out', 'adjustment'] },
|
||||
reason: { type: 'string' },
|
||||
part_id: { type: 'string', format: 'uuid', description: 'ID de la refacción' },
|
||||
quantity: { type: 'number', description: 'Cantidad a ajustar (positivo=entrada, negativo=salida)' },
|
||||
reason: { type: 'string', description: 'Razón del ajuste' },
|
||||
reference: { type: 'string', description: 'Referencia (ej: número de orden)' },
|
||||
},
|
||||
required: ['product_id', 'quantity', 'movement_type'],
|
||||
required: ['part_id', 'quantity', 'reason'],
|
||||
},
|
||||
returns: { type: 'object' },
|
||||
returns: { type: 'object', description: 'Refacción actualizada' },
|
||||
},
|
||||
{
|
||||
name: 'get_inventory_value',
|
||||
@ -62,93 +95,356 @@ export class InventoryToolsService implements McpToolProvider {
|
||||
category: 'inventory',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
warehouse_id: { type: 'string', description: 'ID del almacen (opcional)' },
|
||||
},
|
||||
properties: {},
|
||||
},
|
||||
returns: {
|
||||
returns: { type: 'object', description: 'Valor del inventario' },
|
||||
},
|
||||
{
|
||||
name: 'create_part',
|
||||
description: 'Crea una nueva refacción en el inventario',
|
||||
category: 'inventory',
|
||||
permissions: ['inventory.create'],
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
total_value: { type: 'number' },
|
||||
items_count: { type: 'number' },
|
||||
sku: { type: 'string', description: 'Código SKU único' },
|
||||
name: { type: 'string', description: 'Nombre de la refacción' },
|
||||
description: { type: 'string', description: 'Descripción' },
|
||||
brand: { type: 'string', description: 'Marca' },
|
||||
manufacturer: { type: 'string', description: 'Fabricante' },
|
||||
price: { type: 'number', description: 'Precio de venta' },
|
||||
cost: { type: 'number', description: 'Costo' },
|
||||
min_stock: { type: 'number', description: 'Stock mínimo' },
|
||||
barcode: { type: 'string', description: 'Código de barras' },
|
||||
},
|
||||
required: ['sku', 'name', 'price'],
|
||||
},
|
||||
returns: { type: 'object', description: 'Refacción creada' },
|
||||
},
|
||||
{
|
||||
name: 'list_parts',
|
||||
description: 'Lista refacciones con filtros opcionales',
|
||||
category: 'inventory',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
category_id: { type: 'string', format: 'uuid' },
|
||||
brand: { type: 'string' },
|
||||
search: { type: 'string' },
|
||||
low_stock: { type: 'boolean', description: 'Solo mostrar con stock bajo' },
|
||||
page: { type: 'number', default: 1 },
|
||||
limit: { type: 'number', default: 20 },
|
||||
},
|
||||
},
|
||||
returns: { type: 'object', description: 'Lista paginada de refacciones' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
getHandler(toolName: string): McpToolHandler | undefined {
|
||||
const handlers: Record<string, McpToolHandler> = {
|
||||
search_parts: this.searchParts.bind(this),
|
||||
get_part_details: this.getPartDetails.bind(this),
|
||||
check_stock: this.checkStock.bind(this),
|
||||
get_low_stock_products: this.getLowStockProducts.bind(this),
|
||||
record_inventory_movement: this.recordInventoryMovement.bind(this),
|
||||
get_low_stock_parts: this.getLowStockParts.bind(this),
|
||||
adjust_stock: this.adjustStock.bind(this),
|
||||
get_inventory_value: this.getInventoryValue.bind(this),
|
||||
create_part: this.createPart.bind(this),
|
||||
list_parts: this.listParts.bind(this),
|
||||
};
|
||||
return handlers[toolName];
|
||||
}
|
||||
|
||||
private async checkStock(
|
||||
params: { product_ids?: string[]; warehouse_id?: string },
|
||||
context: McpContext
|
||||
): Promise<any[]> {
|
||||
// TODO: Connect to actual inventory service
|
||||
return [
|
||||
{
|
||||
product_id: 'sample-1',
|
||||
product_name: 'Producto ejemplo',
|
||||
stock: 100,
|
||||
warehouse_id: params.warehouse_id || 'default',
|
||||
message: 'Conectar a InventoryService real',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private async getLowStockProducts(
|
||||
params: { threshold?: number },
|
||||
context: McpContext
|
||||
): Promise<any[]> {
|
||||
// TODO: Connect to actual inventory service
|
||||
const threshold = params.threshold || 10;
|
||||
return [
|
||||
{
|
||||
product_id: 'low-stock-1',
|
||||
product_name: 'Producto bajo stock',
|
||||
current_stock: 5,
|
||||
min_stock: threshold,
|
||||
shortage: threshold - 5,
|
||||
message: 'Conectar a InventoryService real',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private async recordInventoryMovement(
|
||||
params: { product_id: string; quantity: number; movement_type: string; reason?: string },
|
||||
private async searchParts(
|
||||
params: { query: string; limit?: number },
|
||||
context: McpContext
|
||||
): Promise<any> {
|
||||
// TODO: Connect to actual inventory service
|
||||
const parts = await this.partService.search(
|
||||
context.tenantId,
|
||||
params.query,
|
||||
params.limit || 10
|
||||
);
|
||||
|
||||
return {
|
||||
movement_id: 'mov-' + Date.now(),
|
||||
product_id: params.product_id,
|
||||
quantity: params.quantity,
|
||||
movement_type: params.movement_type,
|
||||
reason: params.reason,
|
||||
recorded_by: context.userId,
|
||||
recorded_at: new Date().toISOString(),
|
||||
message: 'Conectar a InventoryService real',
|
||||
success: true,
|
||||
parts: parts.map(part => ({
|
||||
id: part.id,
|
||||
sku: part.sku,
|
||||
name: part.name,
|
||||
brand: part.brand,
|
||||
price: part.price,
|
||||
current_stock: part.currentStock,
|
||||
available_stock: part.currentStock - part.reservedStock,
|
||||
})),
|
||||
count: parts.length,
|
||||
};
|
||||
}
|
||||
|
||||
private async getPartDetails(
|
||||
params: { part_id?: string; sku?: string; barcode?: string },
|
||||
context: McpContext
|
||||
): Promise<any> {
|
||||
let part = null;
|
||||
|
||||
if (params.part_id) {
|
||||
part = await this.partService.findById(context.tenantId, params.part_id);
|
||||
} else if (params.sku) {
|
||||
part = await this.partService.findBySku(context.tenantId, params.sku);
|
||||
} else if (params.barcode) {
|
||||
part = await this.partService.findByBarcode(context.tenantId, params.barcode);
|
||||
}
|
||||
|
||||
if (!part) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Refacción no encontrada',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
part: {
|
||||
id: part.id,
|
||||
sku: part.sku,
|
||||
name: part.name,
|
||||
description: part.description,
|
||||
brand: part.brand,
|
||||
manufacturer: part.manufacturer,
|
||||
compatible_engines: part.compatibleEngines,
|
||||
unit: part.unit,
|
||||
cost: part.cost,
|
||||
price: part.price,
|
||||
current_stock: part.currentStock,
|
||||
reserved_stock: part.reservedStock,
|
||||
available_stock: part.currentStock - part.reservedStock,
|
||||
min_stock: part.minStock,
|
||||
max_stock: part.maxStock,
|
||||
reorder_point: part.reorderPoint,
|
||||
barcode: part.barcode,
|
||||
is_active: part.isActive,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async checkStock(
|
||||
params: { part_ids: string[] },
|
||||
context: McpContext
|
||||
): Promise<any> {
|
||||
const stockInfo = await Promise.all(
|
||||
params.part_ids.map(async (id) => {
|
||||
const part = await this.partService.findById(context.tenantId, id);
|
||||
if (!part) {
|
||||
return {
|
||||
part_id: id,
|
||||
found: false,
|
||||
error: 'No encontrada',
|
||||
};
|
||||
}
|
||||
return {
|
||||
part_id: id,
|
||||
found: true,
|
||||
sku: part.sku,
|
||||
name: part.name,
|
||||
current_stock: part.currentStock,
|
||||
reserved_stock: part.reservedStock,
|
||||
available_stock: part.currentStock - part.reservedStock,
|
||||
min_stock: part.minStock,
|
||||
is_low_stock: part.currentStock <= part.minStock,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
stock: stockInfo,
|
||||
};
|
||||
}
|
||||
|
||||
private async getLowStockParts(
|
||||
params: {},
|
||||
context: McpContext
|
||||
): Promise<any> {
|
||||
const parts = await this.partService.getLowStockParts(context.tenantId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
parts: parts.map(part => ({
|
||||
id: part.id,
|
||||
sku: part.sku,
|
||||
name: part.name,
|
||||
brand: part.brand,
|
||||
current_stock: part.currentStock,
|
||||
min_stock: part.minStock,
|
||||
shortage: part.minStock - part.currentStock,
|
||||
price: part.price,
|
||||
})),
|
||||
count: parts.length,
|
||||
alert: parts.length > 0 ? `${parts.length} refacciones con stock bajo` : 'Stock OK',
|
||||
};
|
||||
}
|
||||
|
||||
private async adjustStock(
|
||||
params: { part_id: string; quantity: number; reason: string; reference?: string },
|
||||
context: McpContext
|
||||
): Promise<any> {
|
||||
try {
|
||||
const dto: StockAdjustmentDto = {
|
||||
quantity: params.quantity,
|
||||
reason: params.reason,
|
||||
reference: params.reference,
|
||||
};
|
||||
|
||||
const part = await this.partService.adjustStock(
|
||||
context.tenantId,
|
||||
params.part_id,
|
||||
dto
|
||||
);
|
||||
|
||||
if (!part) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Refacción no encontrada',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
part: {
|
||||
id: part.id,
|
||||
sku: part.sku,
|
||||
name: part.name,
|
||||
previous_stock: part.currentStock - params.quantity,
|
||||
adjustment: params.quantity,
|
||||
new_stock: part.currentStock,
|
||||
reason: params.reason,
|
||||
},
|
||||
message: params.quantity > 0
|
||||
? `Entrada de ${params.quantity} unidades registrada`
|
||||
: `Salida de ${Math.abs(params.quantity)} unidades registrada`,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async getInventoryValue(
|
||||
params: { warehouse_id?: string },
|
||||
params: {},
|
||||
context: McpContext
|
||||
): Promise<any> {
|
||||
// TODO: Connect to actual inventory service
|
||||
const value = await this.partService.getInventoryValue(context.tenantId);
|
||||
|
||||
return {
|
||||
total_value: 150000.00,
|
||||
items_count: 500,
|
||||
warehouse_id: params.warehouse_id || 'all',
|
||||
currency: 'MXN',
|
||||
message: 'Conectar a InventoryService real',
|
||||
success: true,
|
||||
inventory: {
|
||||
total_cost_value: value.totalCostValue,
|
||||
total_sale_value: value.totalSaleValue,
|
||||
total_items: value.totalItems,
|
||||
low_stock_count: value.lowStockCount,
|
||||
margin: value.totalSaleValue - value.totalCostValue,
|
||||
margin_percent: value.totalCostValue > 0
|
||||
? ((value.totalSaleValue - value.totalCostValue) / value.totalCostValue * 100).toFixed(2)
|
||||
: 0,
|
||||
currency: 'MXN',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async createPart(
|
||||
params: {
|
||||
sku: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
brand?: string;
|
||||
manufacturer?: string;
|
||||
price: number;
|
||||
cost?: number;
|
||||
min_stock?: number;
|
||||
barcode?: string;
|
||||
},
|
||||
context: McpContext
|
||||
): Promise<any> {
|
||||
try {
|
||||
const dto: CreatePartDto = {
|
||||
sku: params.sku,
|
||||
name: params.name,
|
||||
description: params.description,
|
||||
brand: params.brand,
|
||||
manufacturer: params.manufacturer,
|
||||
price: params.price,
|
||||
cost: params.cost,
|
||||
minStock: params.min_stock,
|
||||
barcode: params.barcode,
|
||||
};
|
||||
|
||||
const part = await this.partService.create(context.tenantId, dto);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
part: {
|
||||
id: part.id,
|
||||
sku: part.sku,
|
||||
name: part.name,
|
||||
brand: part.brand,
|
||||
price: part.price,
|
||||
cost: part.cost,
|
||||
current_stock: part.currentStock,
|
||||
},
|
||||
message: `Refacción ${part.sku} creada exitosamente`,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async listParts(
|
||||
params: {
|
||||
category_id?: string;
|
||||
brand?: string;
|
||||
search?: string;
|
||||
low_stock?: boolean;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
},
|
||||
context: McpContext
|
||||
): Promise<any> {
|
||||
const result = await this.partService.findAll(
|
||||
context.tenantId,
|
||||
{
|
||||
categoryId: params.category_id,
|
||||
brand: params.brand,
|
||||
search: params.search,
|
||||
lowStock: params.low_stock,
|
||||
},
|
||||
{
|
||||
page: params.page || 1,
|
||||
limit: params.limit || 20,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
parts: result.data.map(part => ({
|
||||
id: part.id,
|
||||
sku: part.sku,
|
||||
name: part.name,
|
||||
brand: part.brand,
|
||||
price: part.price,
|
||||
current_stock: part.currentStock,
|
||||
available_stock: part.currentStock - part.reservedStock,
|
||||
is_low_stock: part.currentStock <= part.minStock,
|
||||
})),
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
total_pages: result.totalPages,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,124 +1,294 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import {
|
||||
McpToolProvider,
|
||||
McpToolDefinition,
|
||||
McpToolHandler,
|
||||
McpContext,
|
||||
} from '../interfaces';
|
||||
import { ServiceOrderService, CreateServiceOrderDto } from '../../service-management/services/service-order.service';
|
||||
import { ServiceOrderStatus } from '../../service-management/entities/service-order.entity';
|
||||
|
||||
/**
|
||||
* Orders Tools Service
|
||||
* Provides MCP tools for order management.
|
||||
*
|
||||
* TODO: Connect to actual OrdersService when available.
|
||||
* Provides MCP tools for service order management.
|
||||
* Connected to actual ServiceOrderService for real data operations.
|
||||
*/
|
||||
export class OrdersToolsService implements McpToolProvider {
|
||||
private serviceOrderService: ServiceOrderService;
|
||||
|
||||
constructor(dataSource: DataSource) {
|
||||
this.serviceOrderService = new ServiceOrderService(dataSource);
|
||||
}
|
||||
|
||||
getTools(): McpToolDefinition[] {
|
||||
return [
|
||||
{
|
||||
name: 'create_order',
|
||||
description: 'Crea un nuevo pedido',
|
||||
name: 'create_service_order',
|
||||
description: 'Crea una nueva orden de servicio para un vehículo',
|
||||
category: 'orders',
|
||||
permissions: ['orders.create'],
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customer_id: { type: 'string', format: 'uuid', description: 'ID del cliente' },
|
||||
items: {
|
||||
type: 'array',
|
||||
description: 'Items del pedido',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
product_id: { type: 'string' },
|
||||
quantity: { type: 'number' },
|
||||
unit_price: { type: 'number' },
|
||||
},
|
||||
},
|
||||
vehicle_id: { type: 'string', format: 'uuid', description: 'ID del vehículo' },
|
||||
customer_symptoms: { type: 'string', description: 'Síntomas reportados por el cliente' },
|
||||
priority: {
|
||||
type: 'string',
|
||||
enum: ['low', 'normal', 'high', 'urgent'],
|
||||
description: 'Prioridad de la orden'
|
||||
},
|
||||
payment_method: { type: 'string', enum: ['cash', 'card', 'transfer', 'fiado'] },
|
||||
notes: { type: 'string' },
|
||||
assigned_to: { type: 'string', format: 'uuid', description: 'ID del técnico asignado' },
|
||||
bay_id: { type: 'string', format: 'uuid', description: 'ID de la bahía de trabajo' },
|
||||
odometer_in: { type: 'number', description: 'Kilometraje de entrada' },
|
||||
internal_notes: { type: 'string', description: 'Notas internas' },
|
||||
},
|
||||
required: ['customer_id', 'items'],
|
||||
required: ['customer_id', 'vehicle_id'],
|
||||
},
|
||||
returns: { type: 'object' },
|
||||
returns: { type: 'object', description: 'Orden de servicio creada' },
|
||||
},
|
||||
{
|
||||
name: 'get_order_status',
|
||||
description: 'Consulta el estado de un pedido',
|
||||
name: 'get_service_order',
|
||||
description: 'Consulta una orden de servicio por ID o número de orden',
|
||||
category: 'orders',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
order_id: { type: 'string', format: 'uuid' },
|
||||
order_id: { type: 'string', format: 'uuid', description: 'ID de la orden' },
|
||||
order_number: { type: 'string', description: 'Número de orden (ej: OS-2026-00001)' },
|
||||
},
|
||||
required: ['order_id'],
|
||||
},
|
||||
returns: { type: 'object' },
|
||||
returns: { type: 'object', description: 'Detalle de la orden de servicio' },
|
||||
},
|
||||
{
|
||||
name: 'list_service_orders',
|
||||
description: 'Lista órdenes de servicio con filtros opcionales',
|
||||
category: 'orders',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['received', 'diagnosed', 'quoted', 'approved', 'in_progress', 'waiting_parts', 'completed', 'delivered', 'cancelled'],
|
||||
description: 'Filtrar por estado'
|
||||
},
|
||||
priority: { type: 'string', enum: ['low', 'normal', 'high', 'urgent'] },
|
||||
customer_id: { type: 'string', format: 'uuid' },
|
||||
vehicle_id: { type: 'string', format: 'uuid' },
|
||||
assigned_to: { type: 'string', format: 'uuid' },
|
||||
search: { type: 'string', description: 'Buscar en número de orden o síntomas' },
|
||||
page: { type: 'number', default: 1 },
|
||||
limit: { type: 'number', default: 20 },
|
||||
},
|
||||
},
|
||||
returns: { type: 'object', description: 'Lista paginada de órdenes' },
|
||||
},
|
||||
{
|
||||
name: 'update_order_status',
|
||||
description: 'Actualiza el estado de un pedido',
|
||||
description: 'Actualiza el estado de una orden de servicio',
|
||||
category: 'orders',
|
||||
permissions: ['orders.update'],
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
order_id: { type: 'string', format: 'uuid' },
|
||||
order_id: { type: 'string', format: 'uuid', description: 'ID de la orden' },
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['pending', 'confirmed', 'preparing', 'ready', 'delivered', 'cancelled'],
|
||||
enum: ['received', 'diagnosed', 'quoted', 'approved', 'in_progress', 'waiting_parts', 'completed', 'delivered', 'cancelled'],
|
||||
description: 'Nuevo estado'
|
||||
},
|
||||
},
|
||||
required: ['order_id', 'status'],
|
||||
},
|
||||
returns: { type: 'object' },
|
||||
returns: { type: 'object', description: 'Orden actualizada' },
|
||||
},
|
||||
{
|
||||
name: 'get_orders_kanban',
|
||||
description: 'Obtiene órdenes agrupadas por estado para vista Kanban',
|
||||
category: 'orders',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
returns: { type: 'object', description: 'Órdenes agrupadas por estado' },
|
||||
},
|
||||
{
|
||||
name: 'get_orders_dashboard',
|
||||
description: 'Obtiene estadísticas del dashboard de órdenes',
|
||||
category: 'orders',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
returns: { type: 'object', description: 'Estadísticas del dashboard' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
getHandler(toolName: string): McpToolHandler | undefined {
|
||||
const handlers: Record<string, McpToolHandler> = {
|
||||
create_order: this.createOrder.bind(this),
|
||||
get_order_status: this.getOrderStatus.bind(this),
|
||||
create_service_order: this.createServiceOrder.bind(this),
|
||||
get_service_order: this.getServiceOrder.bind(this),
|
||||
list_service_orders: this.listServiceOrders.bind(this),
|
||||
update_order_status: this.updateOrderStatus.bind(this),
|
||||
get_orders_kanban: this.getOrdersKanban.bind(this),
|
||||
get_orders_dashboard: this.getOrdersDashboard.bind(this),
|
||||
};
|
||||
return handlers[toolName];
|
||||
}
|
||||
|
||||
private async createOrder(
|
||||
params: { customer_id: string; items: any[]; payment_method?: string; notes?: string },
|
||||
private async createServiceOrder(
|
||||
params: {
|
||||
customer_id: string;
|
||||
vehicle_id: string;
|
||||
customer_symptoms?: string;
|
||||
priority?: string;
|
||||
assigned_to?: string;
|
||||
bay_id?: string;
|
||||
odometer_in?: number;
|
||||
internal_notes?: string;
|
||||
},
|
||||
context: McpContext
|
||||
): Promise<any> {
|
||||
// TODO: Connect to actual orders service
|
||||
const subtotal = params.items.reduce((sum, item) => sum + (item.quantity * (item.unit_price || 0)), 0);
|
||||
const dto: CreateServiceOrderDto = {
|
||||
customerId: params.customer_id,
|
||||
vehicleId: params.vehicle_id,
|
||||
customerSymptoms: params.customer_symptoms,
|
||||
priority: params.priority as any,
|
||||
assignedTo: params.assigned_to,
|
||||
bayId: params.bay_id,
|
||||
odometerIn: params.odometer_in,
|
||||
internalNotes: params.internal_notes,
|
||||
};
|
||||
|
||||
const order = await this.serviceOrderService.create(
|
||||
context.tenantId,
|
||||
dto,
|
||||
context.userId
|
||||
);
|
||||
|
||||
return {
|
||||
order_id: 'order-' + Date.now(),
|
||||
customer_id: params.customer_id,
|
||||
items: params.items,
|
||||
subtotal,
|
||||
tax: subtotal * 0.16,
|
||||
total: subtotal * 1.16,
|
||||
payment_method: params.payment_method || 'cash',
|
||||
status: 'pending',
|
||||
created_by: context.userId,
|
||||
created_at: new Date().toISOString(),
|
||||
message: 'Conectar a OrdersService real',
|
||||
success: true,
|
||||
order: {
|
||||
id: order.id,
|
||||
order_number: order.orderNumber,
|
||||
status: order.status,
|
||||
priority: order.priority,
|
||||
customer_id: order.customerId,
|
||||
vehicle_id: order.vehicleId,
|
||||
received_at: order.receivedAt,
|
||||
created_by: order.createdBy,
|
||||
},
|
||||
message: `Orden de servicio ${order.orderNumber} creada exitosamente`,
|
||||
};
|
||||
}
|
||||
|
||||
private async getOrderStatus(
|
||||
params: { order_id: string },
|
||||
private async getServiceOrder(
|
||||
params: { order_id?: string; order_number?: string },
|
||||
context: McpContext
|
||||
): Promise<any> {
|
||||
// TODO: Connect to actual orders service
|
||||
let order = null;
|
||||
|
||||
if (params.order_id) {
|
||||
order = await this.serviceOrderService.findById(context.tenantId, params.order_id);
|
||||
} else if (params.order_number) {
|
||||
order = await this.serviceOrderService.findByOrderNumber(context.tenantId, params.order_number);
|
||||
}
|
||||
|
||||
if (!order) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Orden de servicio no encontrada',
|
||||
};
|
||||
}
|
||||
|
||||
// Get order items
|
||||
const items = await this.serviceOrderService.getItems(order.id);
|
||||
|
||||
return {
|
||||
order_id: params.order_id,
|
||||
status: 'pending',
|
||||
customer_name: 'Cliente ejemplo',
|
||||
total: 1160.00,
|
||||
items_count: 3,
|
||||
created_at: new Date().toISOString(),
|
||||
message: 'Conectar a OrdersService real',
|
||||
success: true,
|
||||
order: {
|
||||
id: order.id,
|
||||
order_number: order.orderNumber,
|
||||
status: order.status,
|
||||
priority: order.priority,
|
||||
customer_id: order.customerId,
|
||||
vehicle_id: order.vehicleId,
|
||||
customer_symptoms: order.customerSymptoms,
|
||||
assigned_to: order.assignedTo,
|
||||
bay_id: order.bayId,
|
||||
odometer_in: order.odometerIn,
|
||||
odometer_out: order.odometerOut,
|
||||
labor_total: order.laborTotal,
|
||||
parts_total: order.partsTotal,
|
||||
discount_percent: order.discountPercent,
|
||||
discount_amount: order.discountAmount,
|
||||
tax: order.tax,
|
||||
grand_total: order.grandTotal,
|
||||
received_at: order.receivedAt,
|
||||
started_at: order.startedAt,
|
||||
completed_at: order.completedAt,
|
||||
delivered_at: order.deliveredAt,
|
||||
items: items.map(item => ({
|
||||
id: item.id,
|
||||
type: item.itemType,
|
||||
description: item.description,
|
||||
quantity: item.quantity,
|
||||
unit_price: item.unitPrice,
|
||||
subtotal: item.subtotal,
|
||||
status: item.status,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async listServiceOrders(
|
||||
params: {
|
||||
status?: string;
|
||||
priority?: string;
|
||||
customer_id?: string;
|
||||
vehicle_id?: string;
|
||||
assigned_to?: string;
|
||||
search?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
},
|
||||
context: McpContext
|
||||
): Promise<any> {
|
||||
const result = await this.serviceOrderService.findAll(
|
||||
context.tenantId,
|
||||
{
|
||||
status: params.status as ServiceOrderStatus,
|
||||
priority: params.priority as any,
|
||||
customerId: params.customer_id,
|
||||
vehicleId: params.vehicle_id,
|
||||
assignedTo: params.assigned_to,
|
||||
search: params.search,
|
||||
},
|
||||
{
|
||||
page: params.page || 1,
|
||||
limit: params.limit || 20,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
orders: result.data.map(order => ({
|
||||
id: order.id,
|
||||
order_number: order.orderNumber,
|
||||
status: order.status,
|
||||
priority: order.priority,
|
||||
customer_id: order.customerId,
|
||||
vehicle_id: order.vehicleId,
|
||||
grand_total: order.grandTotal,
|
||||
received_at: order.receivedAt,
|
||||
})),
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
total_pages: result.totalPages,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -126,14 +296,84 @@ export class OrdersToolsService implements McpToolProvider {
|
||||
params: { order_id: string; status: string },
|
||||
context: McpContext
|
||||
): Promise<any> {
|
||||
// TODO: Connect to actual orders service
|
||||
const order = await this.serviceOrderService.update(
|
||||
context.tenantId,
|
||||
params.order_id,
|
||||
{ status: params.status as ServiceOrderStatus }
|
||||
);
|
||||
|
||||
if (!order) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Orden de servicio no encontrada',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
order_id: params.order_id,
|
||||
previous_status: 'pending',
|
||||
new_status: params.status,
|
||||
updated_by: context.userId,
|
||||
updated_at: new Date().toISOString(),
|
||||
message: 'Conectar a OrdersService real',
|
||||
success: true,
|
||||
order: {
|
||||
id: order.id,
|
||||
order_number: order.orderNumber,
|
||||
previous_status: order.status,
|
||||
new_status: params.status,
|
||||
updated_at: order.updatedAt,
|
||||
},
|
||||
message: `Estado actualizado a ${params.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
private async getOrdersKanban(
|
||||
params: {},
|
||||
context: McpContext
|
||||
): Promise<any> {
|
||||
const grouped = await this.serviceOrderService.getOrdersByStatus(context.tenantId);
|
||||
|
||||
// Transform to simpler format for AI consumption
|
||||
const kanban: Record<string, any[]> = {};
|
||||
for (const [status, orders] of Object.entries(grouped)) {
|
||||
kanban[status] = orders.map(order => ({
|
||||
id: order.id,
|
||||
order_number: order.orderNumber,
|
||||
priority: order.priority,
|
||||
customer_id: order.customerId,
|
||||
vehicle_id: order.vehicleId,
|
||||
grand_total: order.grandTotal,
|
||||
received_at: order.receivedAt,
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
kanban,
|
||||
summary: {
|
||||
received: grouped[ServiceOrderStatus.RECEIVED]?.length || 0,
|
||||
diagnosed: grouped[ServiceOrderStatus.DIAGNOSED]?.length || 0,
|
||||
quoted: grouped[ServiceOrderStatus.QUOTED]?.length || 0,
|
||||
approved: grouped[ServiceOrderStatus.APPROVED]?.length || 0,
|
||||
in_progress: grouped[ServiceOrderStatus.IN_PROGRESS]?.length || 0,
|
||||
waiting_parts: grouped[ServiceOrderStatus.WAITING_PARTS]?.length || 0,
|
||||
completed: grouped[ServiceOrderStatus.COMPLETED]?.length || 0,
|
||||
delivered: grouped[ServiceOrderStatus.DELIVERED]?.length || 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async getOrdersDashboard(
|
||||
params: {},
|
||||
context: McpContext
|
||||
): Promise<any> {
|
||||
const stats = await this.serviceOrderService.getDashboardStats(context.tenantId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
dashboard: {
|
||||
total_orders: stats.totalOrders,
|
||||
pending_orders: stats.pendingOrders,
|
||||
in_progress_orders: stats.inProgressOrders,
|
||||
completed_today: stats.completedToday,
|
||||
total_revenue: stats.totalRevenue,
|
||||
average_ticket: stats.averageTicket,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user