feat(statistics): implement top alert stocks, top last sales, top supplier debts, and top selling products endpoints with SQL queries
This commit is contained in:
@@ -92,13 +92,13 @@ export class InventoriesService {
|
||||
console.log(movements)
|
||||
|
||||
const mapped = movements.map(movement => {
|
||||
const { id, quantity, fee, totalCost, avgCost, product } = movement
|
||||
const { id, quantity, unitPrice, totalCost, avgCost, product } = movement
|
||||
if (info === null) {
|
||||
info = {
|
||||
date: movement.createdAt,
|
||||
type: movement.type,
|
||||
quantity: Number(movement.quantity),
|
||||
fee: Number(movement.fee),
|
||||
unitPrice: Number(movement.unitPrice),
|
||||
totalCost: Number(movement.totalCost),
|
||||
referenceType: movement.referenceType,
|
||||
referenceId: movement.referenceId,
|
||||
@@ -107,13 +107,13 @@ export class InventoriesService {
|
||||
}
|
||||
} else {
|
||||
info.quantity += Number(movement.quantity)
|
||||
info.fee += Number(movement.fee)
|
||||
info.unitPrice += Number(movement.unitPrice)
|
||||
info.totalCost += Number(movement.totalCost)
|
||||
}
|
||||
return {
|
||||
id,
|
||||
quantity: Number(quantity),
|
||||
fee: Number(fee),
|
||||
unitPrice: Number(unitPrice),
|
||||
totalCost: Number(totalCost),
|
||||
avgCost: Number(avgCost),
|
||||
product,
|
||||
|
||||
@@ -33,5 +33,5 @@ export class CreateOrderItemDto {
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
fee: number
|
||||
unitPrice: number
|
||||
}
|
||||
|
||||
@@ -170,7 +170,10 @@ export class PosService {
|
||||
|
||||
const newCode = String(Number(lastCode) + 1)
|
||||
|
||||
const totalAmount = data.items.reduce((acc, item) => acc + item.count * item.fee, 0)
|
||||
const totalAmount = data.items.reduce(
|
||||
(acc, item) => acc + item.count * item.unitPrice,
|
||||
0,
|
||||
)
|
||||
|
||||
const preparedOrder: SalesInvoiceCreateInput = {
|
||||
...res,
|
||||
@@ -182,8 +185,8 @@ export class PosService {
|
||||
create: items.map(item => ({
|
||||
product: { connect: { id: Number(item.productId) } },
|
||||
count: item.count,
|
||||
fee: item.fee,
|
||||
total: item.count * item.fee,
|
||||
unitPrice: item.unitPrice,
|
||||
total: item.count * item.unitPrice,
|
||||
})),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
SELECT p.id
|
||||
FROM Products p
|
||||
WHERE (
|
||||
SELECT COALESCE(SUM(sb.quantity), 0)
|
||||
FROM Stock_Balance sb
|
||||
WHERE
|
||||
sb.productId = p.id
|
||||
) < p.minimumStockAlertLevel
|
||||
ORDER BY (
|
||||
p.minimumStockAlertLevel - (
|
||||
SELECT COALESCE(SUM(sb.quantity), 0)
|
||||
FROM Stock_Balance sb
|
||||
WHERE
|
||||
sb.productId = p.id
|
||||
)
|
||||
) DESC
|
||||
LIMIT 10
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Controller, Get } from '@nestjs/common'
|
||||
import { StatisticsService } from './statistics.service'
|
||||
|
||||
@Controller('statistics')
|
||||
export class StatisticsController {
|
||||
constructor(private readonly statisticsService: StatisticsService) {}
|
||||
|
||||
@Get('top-last-sales')
|
||||
getTopLastSales() {
|
||||
return this.statisticsService.getTopLastSales()
|
||||
}
|
||||
|
||||
@Get('top-alert-stocks')
|
||||
getTopAlertStocks() {
|
||||
return this.statisticsService.getTopAlertStocks()
|
||||
}
|
||||
|
||||
@Get('top-supplier-debts')
|
||||
getTopSupplierDebts() {
|
||||
return this.statisticsService.getTopSupplierDebts()
|
||||
}
|
||||
|
||||
@Get('top-sales')
|
||||
getTopSellProducts() {
|
||||
return this.statisticsService.getTopSellProducts()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { PrismaService } from '../../prisma/prisma.service'
|
||||
import { StatisticsController } from './statistics.controller'
|
||||
import { StatisticsService } from './statistics.service'
|
||||
|
||||
@Module({
|
||||
controllers: [StatisticsController],
|
||||
providers: [StatisticsService, PrismaService],
|
||||
})
|
||||
export class StatisticsModule {}
|
||||
@@ -0,0 +1,184 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import { ResponseMapper } from '../../common/response/response-mapper'
|
||||
import { PrismaService } from '../../prisma/prisma.service'
|
||||
|
||||
@Injectable()
|
||||
export class StatisticsService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
private readSqlFile(filename: string): string {
|
||||
const filePath = path.join(__dirname, 'queries', filename)
|
||||
return fs.readFileSync(filePath, 'utf8')
|
||||
}
|
||||
|
||||
async getTopLastSales() {
|
||||
const sales = await this.prisma.salesInvoice.findMany({
|
||||
take: 10,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
customer: { select: { id: true, firstName: true, lastName: true } },
|
||||
posAccount: { select: { id: true, name: true } },
|
||||
items: {
|
||||
include: {
|
||||
product: { select: { id: true, name: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const mapped = sales.map(sale => ({
|
||||
...sale,
|
||||
totalAmount: Number(sale.totalAmount),
|
||||
itemsCount: sale.items.length,
|
||||
}))
|
||||
|
||||
return ResponseMapper.list(mapped)
|
||||
}
|
||||
|
||||
async getTopAlertStocks() {
|
||||
const productIds = await this.prisma.$queryRaw<{ id: number }[]>`
|
||||
SELECT p.id
|
||||
FROM Products p
|
||||
WHERE (SELECT COALESCE(SUM(sb.quantity), 0) FROM Stock_Balance sb WHERE sb.productId = p.id) < p.minimumStockAlertLevel
|
||||
-- ORDER BY (p.minimumStockAlertLevel - (SELECT COALESCE(SUM(sb.quantity), 0) FROM stock_balance sb WHERE sb.productId = p.id)) DESC
|
||||
LIMIT 10
|
||||
`
|
||||
|
||||
const ids = productIds.map(p => p.id)
|
||||
|
||||
if (ids.length === 0) {
|
||||
return ResponseMapper.list([])
|
||||
}
|
||||
|
||||
const products = await this.prisma.product.findMany({
|
||||
where: { id: { in: ids } },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
sku: true,
|
||||
minimumStockAlertLevel: true,
|
||||
stockBalances: {
|
||||
select: {
|
||||
quantity: true,
|
||||
inventory: { select: { id: true, name: true } },
|
||||
},
|
||||
},
|
||||
category: { select: { id: true, name: true } },
|
||||
brand: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
const mappedData = products.map(product => ({
|
||||
...product,
|
||||
stock: product.stockBalances.reduce((acc, sb) => acc + Number(sb.quantity), 0),
|
||||
deficit:
|
||||
Number(product.minimumStockAlertLevel) -
|
||||
product.stockBalances.reduce((acc, sb) => acc + Number(sb.quantity), 0),
|
||||
}))
|
||||
|
||||
return ResponseMapper.list(mappedData)
|
||||
}
|
||||
|
||||
async getTopSupplierDebts() {
|
||||
// Get unpaid receipts
|
||||
const receipts = await this.prisma.purchaseReceipt.findMany({
|
||||
where: {
|
||||
status: { not: 'PAID' },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
totalAmount: true,
|
||||
paidAmount: true,
|
||||
supplierId: true,
|
||||
},
|
||||
})
|
||||
|
||||
const debtMap = new Map<
|
||||
number,
|
||||
{ supplierId: number; totalDebt: number; unpaidReceipts: number }
|
||||
>()
|
||||
|
||||
for (const receipt of receipts) {
|
||||
const debt = Number(receipt.totalAmount) - Number(receipt.paidAmount)
|
||||
if (debt > 0) {
|
||||
const existing = debtMap.get(receipt.supplierId)
|
||||
if (existing) {
|
||||
existing.totalDebt += debt
|
||||
existing.unpaidReceipts += 1
|
||||
} else {
|
||||
debtMap.set(receipt.supplierId, {
|
||||
supplierId: receipt.supplierId,
|
||||
totalDebt: debt,
|
||||
unpaidReceipts: 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const supplierIds = Array.from(debtMap.keys())
|
||||
const suppliers = await this.prisma.supplier.findMany({
|
||||
where: { id: { in: supplierIds } },
|
||||
select: { id: true, firstName: true, lastName: true },
|
||||
})
|
||||
|
||||
const supplierMap = new Map(
|
||||
suppliers.map(s => [s.id, `${s.firstName} ${s.lastName}`.trim()]),
|
||||
)
|
||||
|
||||
const mapped = Array.from(debtMap.values())
|
||||
.map(item => ({
|
||||
id: item.supplierId,
|
||||
name: supplierMap.get(item.supplierId) || 'Unknown',
|
||||
totalDebt: item.totalDebt,
|
||||
unpaidReceipts: item.unpaidReceipts,
|
||||
}))
|
||||
.sort((a, b) => b.totalDebt - a.totalDebt)
|
||||
.slice(0, 10)
|
||||
|
||||
return ResponseMapper.list(mapped)
|
||||
}
|
||||
|
||||
async getTopSellProducts() {
|
||||
const productData = await this.prisma.$queryRaw<any[]>`
|
||||
SELECT p.id, p.name, p.sku, COALESCE(SUM(sii.count), 0) as totalSold
|
||||
FROM Products p
|
||||
LEFT JOIN Sales_Invoice_Items sii ON p.id = sii.productId
|
||||
GROUP BY p.id
|
||||
HAVING totalSold > 0
|
||||
ORDER BY totalSold DESC
|
||||
LIMIT 10
|
||||
`
|
||||
|
||||
const ids = productData.map(p => p.id)
|
||||
|
||||
if (ids.length === 0) {
|
||||
return ResponseMapper.list([])
|
||||
}
|
||||
|
||||
const products = await this.prisma.product.findMany({
|
||||
where: { id: { in: ids } },
|
||||
select: {
|
||||
id: true,
|
||||
category: { select: { id: true, name: true } },
|
||||
brand: { select: { id: true, name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
const productMap = new Map(
|
||||
products.map(p => [p.id, { category: p.category, brand: p.brand }]),
|
||||
)
|
||||
|
||||
const mapped = productData.map(product => ({
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
sku: product.sku,
|
||||
totalSold: Number(product.totalSold),
|
||||
category: productMap.get(product.id)?.category,
|
||||
brand: productMap.get(product.id)?.brand,
|
||||
}))
|
||||
|
||||
return ResponseMapper.list(mapped)
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,8 @@ export class SupplierInvoicesService {
|
||||
select: {
|
||||
id: true,
|
||||
count: true,
|
||||
fee: true,
|
||||
total: true,
|
||||
unitPrice: true,
|
||||
totalAmount: true,
|
||||
product: {
|
||||
select: {
|
||||
id: true,
|
||||
@@ -149,6 +149,8 @@ export class SupplierInvoicesService {
|
||||
|
||||
async createPayment(invoiceId: number, data: CreateReceiptPaymentDto) {
|
||||
const { bankAccountId, ...rest } = data
|
||||
console.log(data)
|
||||
|
||||
const payment = await this.prisma.purchaseReceiptPayments.create({
|
||||
data: {
|
||||
...rest,
|
||||
|
||||
Reference in New Issue
Block a user