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 }