EPIC-005 - Financial Module: - Add AccountMapping entity for GL account configuration - Create GLPostingService for automatic journal entries - Integrate GL posting with invoice validation - Fix tax calculation in invoice lines EPIC-006 - Inventory Automation: - Integrate FIFO valuation with pickings - Create ReorderAlertsService for stock monitoring - Add lot validation for tracked products - Integrate valuation with inventory adjustments EPIC-007 - CRM Improvements: - Create ActivitiesService for activity management - Create ForecastingService for pipeline analytics - Add win/loss reporting and user performance metrics EPIC-008 - Project Billing: - Create BillingService with billing rate management - Add getUnbilledTimesheets and createInvoiceFromTimesheets - Support grouping options for invoice generation EPIC-009 - HR-Projects Integration: - Create HRIntegrationService for employee-user linking - Add employee cost rate management - Implement project profitability calculations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
178 lines
8.4 KiB
TypeScript
178 lines
8.4 KiB
TypeScript
import { Request, Response, NextFunction, Router } from 'express';
|
|
import { SalesService } from '../services';
|
|
import { CreateQuotationDto, UpdateQuotationDto, CreateSalesOrderDto, UpdateSalesOrderDto } from '../dto';
|
|
|
|
export class QuotationsController {
|
|
public router: Router;
|
|
constructor(private readonly salesService: SalesService) {
|
|
this.router = Router();
|
|
this.router.get('/', this.findAll.bind(this));
|
|
this.router.get('/:id', this.findOne.bind(this));
|
|
this.router.post('/', this.create.bind(this));
|
|
this.router.patch('/:id', this.update.bind(this));
|
|
this.router.delete('/:id', this.delete.bind(this));
|
|
this.router.post('/:id/convert', this.convert.bind(this));
|
|
}
|
|
|
|
private async findAll(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) { res.status(400).json({ error: 'Tenant ID required' }); return; }
|
|
const { partnerId, status, userId, limit, offset } = req.query;
|
|
const result = await this.salesService.findAllQuotations({ tenantId, partnerId: partnerId as string, status: status as string, userId: userId as string, limit: limit ? parseInt(limit as string) : undefined, offset: offset ? parseInt(offset as string) : undefined });
|
|
res.json(result);
|
|
} catch (e) { next(e); }
|
|
}
|
|
|
|
private async findOne(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) { res.status(400).json({ error: 'Tenant ID required' }); return; }
|
|
const quotation = await this.salesService.findQuotation(req.params.id, tenantId);
|
|
if (!quotation) { res.status(404).json({ error: 'Not found' }); return; }
|
|
res.json({ data: quotation });
|
|
} catch (e) { next(e); }
|
|
}
|
|
|
|
private async create(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
const userId = req.headers['x-user-id'] as string;
|
|
if (!tenantId) { res.status(400).json({ error: 'Tenant ID required' }); return; }
|
|
const quotation = await this.salesService.createQuotation(tenantId, req.body, userId);
|
|
res.status(201).json({ data: quotation });
|
|
} catch (e) { next(e); }
|
|
}
|
|
|
|
private async update(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
const userId = req.headers['x-user-id'] as string;
|
|
if (!tenantId) { res.status(400).json({ error: 'Tenant ID required' }); return; }
|
|
const quotation = await this.salesService.updateQuotation(req.params.id, tenantId, req.body, userId);
|
|
if (!quotation) { res.status(404).json({ error: 'Not found' }); return; }
|
|
res.json({ data: quotation });
|
|
} catch (e) { next(e); }
|
|
}
|
|
|
|
private async delete(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) { res.status(400).json({ error: 'Tenant ID required' }); return; }
|
|
const deleted = await this.salesService.deleteQuotation(req.params.id, tenantId);
|
|
if (!deleted) { res.status(404).json({ error: 'Not found' }); return; }
|
|
res.status(204).send();
|
|
} catch (e) { next(e); }
|
|
}
|
|
|
|
private async convert(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
const userId = req.headers['x-user-id'] as string;
|
|
if (!tenantId) { res.status(400).json({ error: 'Tenant ID required' }); return; }
|
|
const order = await this.salesService.convertQuotationToOrder(req.params.id, tenantId, userId);
|
|
res.json({ data: order });
|
|
} catch (e) { next(e); }
|
|
}
|
|
}
|
|
|
|
export class SalesOrdersController {
|
|
public router: Router;
|
|
constructor(private readonly salesService: SalesService) {
|
|
this.router = Router();
|
|
this.router.get('/', this.findAll.bind(this));
|
|
this.router.get('/:id', this.findOne.bind(this));
|
|
this.router.post('/', this.create.bind(this));
|
|
this.router.patch('/:id', this.update.bind(this));
|
|
this.router.delete('/:id', this.delete.bind(this));
|
|
this.router.post('/:id/confirm', this.confirm.bind(this));
|
|
this.router.post('/:id/ship', this.ship.bind(this));
|
|
this.router.post('/:id/deliver', this.deliver.bind(this));
|
|
}
|
|
|
|
private async findAll(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) { res.status(400).json({ error: 'Tenant ID required' }); return; }
|
|
const { partnerId, status, userId, limit, offset } = req.query;
|
|
const result = await this.salesService.findAllOrders({ tenantId, partnerId: partnerId as string, status: status as string, userId: userId as string, limit: limit ? parseInt(limit as string) : undefined, offset: offset ? parseInt(offset as string) : undefined });
|
|
res.json(result);
|
|
} catch (e) { next(e); }
|
|
}
|
|
|
|
private async findOne(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) { res.status(400).json({ error: 'Tenant ID required' }); return; }
|
|
const order = await this.salesService.findOrder(req.params.id, tenantId);
|
|
if (!order) { res.status(404).json({ error: 'Not found' }); return; }
|
|
res.json({ data: order });
|
|
} catch (e) { next(e); }
|
|
}
|
|
|
|
private async create(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
const userId = req.headers['x-user-id'] as string;
|
|
if (!tenantId) { res.status(400).json({ error: 'Tenant ID required' }); return; }
|
|
const order = await this.salesService.createSalesOrder(tenantId, req.body, userId);
|
|
res.status(201).json({ data: order });
|
|
} catch (e) { next(e); }
|
|
}
|
|
|
|
private async update(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
const userId = req.headers['x-user-id'] as string;
|
|
if (!tenantId) { res.status(400).json({ error: 'Tenant ID required' }); return; }
|
|
const order = await this.salesService.updateSalesOrder(req.params.id, tenantId, req.body, userId);
|
|
if (!order) { res.status(404).json({ error: 'Not found' }); return; }
|
|
res.json({ data: order });
|
|
} catch (e) { next(e); }
|
|
}
|
|
|
|
private async delete(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) { res.status(400).json({ error: 'Tenant ID required' }); return; }
|
|
const deleted = await this.salesService.deleteSalesOrder(req.params.id, tenantId);
|
|
if (!deleted) { res.status(404).json({ error: 'Not found' }); return; }
|
|
res.status(204).send();
|
|
} catch (e) { next(e); }
|
|
}
|
|
|
|
private async confirm(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
const userId = req.headers['x-user-id'] as string;
|
|
if (!tenantId) { res.status(400).json({ error: 'Tenant ID required' }); return; }
|
|
const order = await this.salesService.confirmOrder(req.params.id, tenantId, userId);
|
|
if (!order) { res.status(400).json({ error: 'Cannot confirm' }); return; }
|
|
res.json({ data: order });
|
|
} catch (e) { next(e); }
|
|
}
|
|
|
|
private async ship(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
const userId = req.headers['x-user-id'] as string;
|
|
const { trackingNumber, carrier } = req.body;
|
|
if (!tenantId) { res.status(400).json({ error: 'Tenant ID required' }); return; }
|
|
const order = await this.salesService.shipOrder(req.params.id, tenantId, trackingNumber, carrier, userId);
|
|
if (!order) { res.status(400).json({ error: 'Cannot ship' }); return; }
|
|
res.json({ data: order });
|
|
} catch (e) { next(e); }
|
|
}
|
|
|
|
private async deliver(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
const userId = req.headers['x-user-id'] as string;
|
|
if (!tenantId) { res.status(400).json({ error: 'Tenant ID required' }); return; }
|
|
const order = await this.salesService.deliverOrder(req.params.id, tenantId, userId);
|
|
if (!order) { res.status(400).json({ error: 'Cannot deliver' }); return; }
|
|
res.json({ data: order });
|
|
} catch (e) { next(e); }
|
|
}
|
|
}
|