292 lines
9.0 KiB
Go
292 lines
9.0 KiB
Go
package repository
|
|
|
|
import (
|
|
"database/sql"
|
|
|
|
"github.com/wmms/wmms/internal/model"
|
|
"github.com/wmms/wmms/pkg/database"
|
|
)
|
|
|
|
type StockRepo struct{}
|
|
|
|
func NewStockRepo() *StockRepo {
|
|
return &StockRepo{}
|
|
}
|
|
|
|
func (r *StockRepo) GetAllStock(search, category string, limit, offset int) ([]model.Stock, int64, error) {
|
|
countQuery := `SELECT COUNT(*) FROM materials WHERE 1=1`
|
|
var countArgs []interface{}
|
|
|
|
if search != "" {
|
|
countQuery += ` AND (material_code LIKE ? OR name LIKE ?)`
|
|
searchPattern := "%" + search + "%"
|
|
countArgs = append(countArgs, searchPattern, searchPattern)
|
|
}
|
|
if category != "" {
|
|
countQuery += ` AND category = ?`
|
|
countArgs = append(countArgs, category)
|
|
}
|
|
|
|
var total int64
|
|
err := database.DB.QueryRow(countQuery, countArgs...).Scan(&total)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
query := `SELECT id, material_code, name, spec, category, unit, current_stock, warning_stock FROM materials WHERE 1=1`
|
|
var args []interface{}
|
|
|
|
if search != "" {
|
|
query += ` AND (material_code LIKE ? OR name LIKE ?)`
|
|
searchPattern := "%" + search + "%"
|
|
args = append(args, searchPattern, searchPattern)
|
|
}
|
|
if category != "" {
|
|
query += ` AND category = ?`
|
|
args = append(args, category)
|
|
}
|
|
|
|
query += ` ORDER BY id LIMIT ? OFFSET ?`
|
|
args = append(args, limit, offset)
|
|
|
|
rows, err := database.DB.Query(query, args...)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var stocks []model.Stock
|
|
for rows.Next() {
|
|
var s model.Stock
|
|
err := rows.Scan(&s.MaterialID, &s.MaterialCode, &s.Name, &s.Spec, &s.Category, &s.Unit, &s.CurrentStock, &s.WarningStock)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
stocks = append(stocks, s)
|
|
}
|
|
return stocks, total, nil
|
|
}
|
|
|
|
func (r *StockRepo) GetByMaterialID(materialID int64) (*model.Stock, error) {
|
|
s := &model.Stock{}
|
|
query := `SELECT id, material_code, name, spec, category, unit, current_stock, warning_stock FROM materials WHERE id = ?`
|
|
err := database.DB.QueryRow(query, materialID).Scan(&s.MaterialID, &s.MaterialCode, &s.Name, &s.Spec, &s.Category, &s.Unit, &s.CurrentStock, &s.WarningStock)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
func (r *StockRepo) GetWarningStock(limit, offset int) ([]model.Stock, int64, error) {
|
|
countQuery := `SELECT COUNT(*) FROM materials WHERE current_stock < warning_stock`
|
|
var total int64
|
|
err := database.DB.QueryRow(countQuery).Scan(&total)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
query := `SELECT id, material_code, name, spec, category, unit, current_stock, warning_stock FROM materials WHERE current_stock < warning_stock ORDER BY id LIMIT ? OFFSET ?`
|
|
rows, err := database.DB.Query(query, limit, offset)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var stocks []model.Stock
|
|
for rows.Next() {
|
|
var s model.Stock
|
|
err := rows.Scan(&s.MaterialID, &s.MaterialCode, &s.Name, &s.Spec, &s.Category, &s.Unit, &s.CurrentStock, &s.WarningStock)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
stocks = append(stocks, s)
|
|
}
|
|
return stocks, total, nil
|
|
}
|
|
|
|
func (r *StockRepo) CreateLog(log *model.StockLog) error {
|
|
query := `INSERT INTO stock_logs (material_id, type, quantity, related_order_type, related_order_id) VALUES (?, ?, ?, ?, ?)`
|
|
result, err := database.DB.Exec(query, log.MaterialID, log.Type, log.Quantity, log.RelatedOrderType, log.RelatedOrderID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id, err := result.LastInsertId()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.ID = id
|
|
return nil
|
|
}
|
|
|
|
func (r *StockRepo) GetLogsByMaterialID(materialID int64, limit, offset int) ([]model.StockLog, int64, error) {
|
|
countQuery := `SELECT COUNT(*) FROM stock_logs WHERE material_id = ?`
|
|
var total int64
|
|
err := database.DB.QueryRow(countQuery, materialID).Scan(&total)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
query := `SELECT id, material_id, type, quantity, related_order_type, related_order_id, created_at FROM stock_logs WHERE material_id = ? ORDER BY id LIMIT ? OFFSET ?`
|
|
rows, err := database.DB.Query(query, materialID, limit, offset)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var logs []model.StockLog
|
|
for rows.Next() {
|
|
var log model.StockLog
|
|
err := rows.Scan(&log.ID, &log.MaterialID, &log.Type, &log.Quantity, &log.RelatedOrderType, &log.RelatedOrderID, &log.CreatedAt)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
logs = append(logs, log)
|
|
}
|
|
return logs, total, nil
|
|
}
|
|
|
|
func (r *StockRepo) GetAllLogs(limit, offset int) ([]model.StockLog, int64, error) {
|
|
countQuery := `SELECT COUNT(*) FROM stock_logs`
|
|
var total int64
|
|
err := database.DB.QueryRow(countQuery).Scan(&total)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Compute running_balance as cumulative sum of (in=+quantity, out=-quantity) ordered by id ASC,
|
|
// then reverse to show newest first while preserving correct cumulative balance for each row
|
|
query := `
|
|
SELECT id, material_id, type, quantity, related_order_type, related_order_id, created_at
|
|
FROM (
|
|
SELECT *,
|
|
(SELECT COALESCE(SUM(CASE WHEN type='in' THEN quantity ELSE -quantity END), 0)
|
|
FROM stock_logs s2 WHERE s2.id <= stock_logs.id) as running_balance
|
|
FROM stock_logs
|
|
) as t
|
|
ORDER BY id DESC
|
|
LIMIT ? OFFSET ?`
|
|
rows, err := database.DB.Query(query, limit, offset)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var logs []model.StockLog
|
|
for rows.Next() {
|
|
var log model.StockLog
|
|
err := rows.Scan(&log.ID, &log.MaterialID, &log.Type, &log.Quantity, &log.RelatedOrderType, &log.RelatedOrderID, &log.CreatedAt)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
logs = append(logs, log)
|
|
}
|
|
return logs, total, nil
|
|
}
|
|
|
|
// GetAllLogsWithBalance returns logs ordered newest-first with correct running balance.
|
|
// Balance = stock level BEFORE this log's change was applied.
|
|
// Strategy: Walk rows from oldest to newest within the page, accumulating balance.
|
|
// The starting balance for the oldest row is: current_stock - sum(all deltas for this material newer than the page).
|
|
func (r *StockRepo) GetAllLogsWithBalance(limit, offset int) ([]model.StockLog, int64, error) {
|
|
var total int64
|
|
if err := database.DB.QueryRow(`SELECT COUNT(*) FROM stock_logs`).Scan(&total); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Fetch page rows ordered newest-first (id DESC).
|
|
rows, err := database.DB.Query(`
|
|
SELECT id, material_id, type, quantity, related_order_type, related_order_id, created_at
|
|
FROM stock_logs ORDER BY id DESC LIMIT ? OFFSET ?`, limit, offset)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
type rowData struct {
|
|
log model.StockLog
|
|
delta int
|
|
}
|
|
var pageRows []rowData
|
|
materialIDs := make(map[int64]bool)
|
|
for rows.Next() {
|
|
var r rowData
|
|
if err := rows.Scan(&r.log.ID, &r.log.MaterialID, &r.log.Type, &r.log.Quantity,
|
|
&r.log.RelatedOrderType, &r.log.RelatedOrderID, &r.log.CreatedAt); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if r.log.Type == "in" {
|
|
r.delta = r.log.Quantity
|
|
} else {
|
|
r.delta = -r.log.Quantity
|
|
}
|
|
pageRows = append(pageRows, r)
|
|
materialIDs[r.log.MaterialID] = true
|
|
}
|
|
|
|
if len(pageRows) == 0 {
|
|
return []model.StockLog{}, total, nil
|
|
}
|
|
|
|
// Get initialBalance: current_stock of this material minus all deltas for this material.
|
|
// This gives the balance BEFORE the oldest log in this page was applied.
|
|
initialBalances := make(map[int64]int)
|
|
for mid := range materialIDs {
|
|
var delta int
|
|
database.DB.QueryRow(`
|
|
SELECT COALESCE(SUM(CASE WHEN type='in' THEN quantity ELSE -quantity END), 0)
|
|
FROM stock_logs WHERE material_id = ?`, mid).Scan(&delta)
|
|
var currentStock int
|
|
database.DB.QueryRow(`SELECT current_stock FROM materials WHERE id = ?`, mid).Scan(¤tStock)
|
|
initialBalances[mid] = currentStock - delta
|
|
}
|
|
|
|
// Process from OLDEST to NEWEST within the page (reverse order of our DESC result).
|
|
// balance_for_oldest = current_stock - newer_than_page_delta
|
|
// balance_after_row = balance_before_row + delta
|
|
for i := len(pageRows) - 1; i >= 0; i-- {
|
|
r := &pageRows[i]
|
|
bal := initialBalances[r.log.MaterialID]
|
|
if r.log.Type == "in" {
|
|
bal += r.log.Quantity
|
|
} else {
|
|
bal -= r.log.Quantity
|
|
}
|
|
r.log.RunningBalance = bal
|
|
initialBalances[r.log.MaterialID] = bal
|
|
}
|
|
|
|
var logs []model.StockLog
|
|
for _, r := range pageRows {
|
|
logs = append(logs, r.log)
|
|
}
|
|
return logs, total, nil
|
|
}
|
|
|
|
func (r *StockRepo) DeleteLogs(orderType string, orderID int64) error {
|
|
query := `DELETE FROM stock_logs WHERE related_order_type = ? AND related_order_id = ?`
|
|
_, err := database.DB.Exec(query, orderType, orderID)
|
|
return err
|
|
}
|
|
|
|
// CreateLogTx creates a stock log within a transaction
|
|
func (r *StockRepo) CreateLogTx(tx *sql.Tx, log *model.StockLog) error {
|
|
query := `INSERT INTO stock_logs (material_id, type, quantity, related_order_type, related_order_id) VALUES (?, ?, ?, ?, ?)`
|
|
result, err := tx.Exec(query, log.MaterialID, log.Type, log.Quantity, log.RelatedOrderType, log.RelatedOrderID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id, err := result.LastInsertId()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.ID = id
|
|
return nil
|
|
}
|
|
|
|
// DeleteLogsTx deletes stock logs within a transaction
|
|
func (r *StockRepo) DeleteLogsTx(tx *sql.Tx, orderType string, orderID int64) error {
|
|
query := `DELETE FROM stock_logs WHERE related_order_type = ? AND related_order_id = ?`
|
|
_, err := tx.Exec(query, orderType, orderID)
|
|
return err
|
|
}
|