feat: add endpoint and logic for retrieving transaction chart data, including SQL queries, models, and service integration

This commit is contained in:
Tran Anh Tuan
2026-05-14 10:41:25 +07:00
parent 0a56dfeb61
commit 96bc22942b
11 changed files with 413 additions and 0 deletions

View File

@@ -63,3 +63,17 @@ JOIN rooms r ON cab.room_id = r.id
WHERE ci.status != 'normal' WHERE ci.status != 'normal'
AND (sqlc.narg('warehouse_id')::bigint IS NULL OR r.warehouse_id = sqlc.narg('warehouse_id')::bigint) AND (sqlc.narg('warehouse_id')::bigint IS NULL OR r.warehouse_id = sqlc.narg('warehouse_id')::bigint)
ORDER BY ci.updated_at DESC; ORDER BY ci.updated_at DESC;
-- name: GetTransactionChartData :many
SELECT DATE(st.created_at) AS date, st.transaction_type, COALESCE(SUM(st.quantity), 0)::bigint AS total_quantity
FROM stock_transactions st
JOIN containers con ON st.container_id = con.id
JOIN shelves s ON con.shelf_id = s.id
JOIN cabinets cab ON s.cabinet_id = cab.id
JOIN rooms r ON cab.room_id = r.id
WHERE st.transaction_type IN ('import', 'export')
AND st.created_at >= sqlc.arg('start_date')::timestamptz
AND st.created_at < sqlc.arg('end_date')::timestamptz
AND (sqlc.narg('warehouse_id')::bigint IS NULL OR r.warehouse_id = sqlc.narg('warehouse_id')::bigint)
GROUP BY DATE(st.created_at), st.transaction_type
ORDER BY DATE(st.created_at) ASC;

View File

@@ -2340,6 +2340,68 @@ const docTemplate = `{
} }
} }
}, },
"/v1/dashboard/transactions-chart": {
"get": {
"description": "Retrieve import/export transaction quantities grouped by date for chart display",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"dashboard"
],
"summary": "Get transactions chart data",
"parameters": [
{
"type": "string",
"default": "7d",
"description": "Time period: today, 7d, 30d, this_month",
"name": "period",
"in": "query"
},
{
"type": "integer",
"description": "Filter by warehouse ID",
"name": "warehouse_id",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.TransactionChartData"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/v1/invoice-config-items": { "/v1/invoice-config-items": {
"get": { "get": {
"description": "Retrieve a list of all invoice config items", "description": "Retrieve a list of all invoice config items",
@@ -4512,6 +4574,31 @@ const docTemplate = `{
} }
} }
}, },
"models.TransactionChartData": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/models.TransactionChartItem"
}
}
}
},
"models.TransactionChartItem": {
"type": "object",
"properties": {
"date": {
"type": "string"
},
"exportQuantity": {
"type": "integer"
},
"importQuantity": {
"type": "integer"
}
}
},
"models.Warehouse": { "models.Warehouse": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -2334,6 +2334,68 @@
} }
} }
}, },
"/v1/dashboard/transactions-chart": {
"get": {
"description": "Retrieve import/export transaction quantities grouped by date for chart display",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"dashboard"
],
"summary": "Get transactions chart data",
"parameters": [
{
"type": "string",
"default": "7d",
"description": "Time period: today, 7d, 30d, this_month",
"name": "period",
"in": "query"
},
{
"type": "integer",
"description": "Filter by warehouse ID",
"name": "warehouse_id",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.TransactionChartData"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/v1/invoice-config-items": { "/v1/invoice-config-items": {
"get": { "get": {
"description": "Retrieve a list of all invoice config items", "description": "Retrieve a list of all invoice config items",
@@ -4506,6 +4568,31 @@
} }
} }
}, },
"models.TransactionChartData": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/models.TransactionChartItem"
}
}
}
},
"models.TransactionChartItem": {
"type": "object",
"properties": {
"date": {
"type": "string"
},
"exportQuantity": {
"type": "integer"
},
"importQuantity": {
"type": "integer"
}
}
},
"models.Warehouse": { "models.Warehouse": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -352,6 +352,22 @@ definitions:
totalTypes: totalTypes:
type: integer type: integer
type: object type: object
models.TransactionChartData:
properties:
items:
items:
$ref: '#/definitions/models.TransactionChartItem'
type: array
type: object
models.TransactionChartItem:
properties:
date:
type: string
exportQuantity:
type: integer
importQuantity:
type: integer
type: object
models.Warehouse: models.Warehouse:
properties: properties:
address: address:
@@ -2546,6 +2562,45 @@ paths:
summary: Get dashboard summary summary: Get dashboard summary
tags: tags:
- dashboard - dashboard
/v1/dashboard/transactions-chart:
get:
consumes:
- application/json
description: Retrieve import/export transaction quantities grouped by date for
chart display
parameters:
- default: 7d
description: 'Time period: today, 7d, 30d, this_month'
in: query
name: period
type: string
- description: Filter by warehouse ID
in: query
name: warehouse_id
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.SuccessResponse'
- properties:
data:
$ref: '#/definitions/models.TransactionChartData'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: Get transactions chart data
tags:
- dashboard
/v1/invoice-config-items: /v1/invoice-config-items:
get: get:
consumes: consumes:

View File

@@ -58,3 +58,11 @@ func ToDomainAnomalyItem(r db.GetAnomalyItemsRow) models.AnomalyItem {
ComponentUnit: r.ComponentUnit, ComponentUnit: r.ComponentUnit,
} }
} }
func ToDomainTransactionChartRow(r db.GetTransactionChartDataRow) models.TransactionChartRow {
return models.TransactionChartRow{
Date: r.Date.Time,
TransactionType: string(r.TransactionType),
TotalQuantity: r.TotalQuantity,
}
}

View File

@@ -52,3 +52,19 @@ type AnomalyItem struct {
ComponentName string `json:"componentName"` ComponentName string `json:"componentName"`
ComponentUnit string `json:"componentUnit"` ComponentUnit string `json:"componentUnit"`
} }
type TransactionChartRow struct {
Date time.Time
TransactionType string
TotalQuantity int64
}
type TransactionChartItem struct {
Date string `json:"date"`
ImportQuantity int64 `json:"importQuantity"`
ExportQuantity int64 `json:"exportQuantity"`
}
type TransactionChartData struct {
Items []TransactionChartItem `json:"items"`
}

View File

@@ -2,6 +2,8 @@ package repositories
import ( import (
"context" "context"
"sort"
"time"
"wm-backend/internal/mapper" "wm-backend/internal/mapper"
"wm-backend/internal/models" "wm-backend/internal/models"
db "wm-backend/sqlc_gen" db "wm-backend/sqlc_gen"
@@ -83,3 +85,43 @@ func GetAnomalyItems(ctx context.Context, queries *db.Queries, warehouseID pgtyp
} }
return items, nil return items, nil
} }
func GetTransactionChartData(ctx context.Context, queries *db.Queries, startDate, endDate time.Time, warehouseID pgtype.Int8) (models.TransactionChartData, error) {
results, err := queries.GetTransactionChartData(ctx, db.GetTransactionChartDataParams{
StartDate: startDate,
EndDate: endDate,
WarehouseID: warehouseID,
})
if err != nil {
return models.TransactionChartData{}, err
}
dateMap := make(map[string]*models.TransactionChartItem)
for _, r := range results {
row := mapper.ToDomainTransactionChartRow(r)
dateStr := row.Date.Format("2006-01-02")
if _, exists := dateMap[dateStr]; !exists {
dateMap[dateStr] = &models.TransactionChartItem{
Date: dateStr,
ImportQuantity: 0,
ExportQuantity: 0,
}
}
switch row.TransactionType {
case "import":
dateMap[dateStr].ImportQuantity = row.TotalQuantity
case "export":
dateMap[dateStr].ExportQuantity = row.TotalQuantity
}
}
items := make([]models.TransactionChartItem, 0, len(dateMap))
for _, item := range dateMap {
items = append(items, *item)
}
sort.Slice(items, func(i, j int) bool {
return items[i].Date < items[j].Date
})
return models.TransactionChartData{Items: items}, nil
}

View File

@@ -158,6 +158,7 @@ func NewRouter() *gin.Engine {
dashboard.GET("/summary", utils.AsyncHandler(services.DashboardSummary)) dashboard.GET("/summary", utils.AsyncHandler(services.DashboardSummary))
dashboard.GET("/stock-alerts", utils.AsyncHandler(services.DashboardStockAlerts)) dashboard.GET("/stock-alerts", utils.AsyncHandler(services.DashboardStockAlerts))
dashboard.GET("/anomalies", utils.AsyncHandler(services.DashboardAnomalies)) dashboard.GET("/anomalies", utils.AsyncHandler(services.DashboardAnomalies))
dashboard.GET("/transactions-chart", utils.AsyncHandler(services.DashboardTransactionsChart))
} }
} }
} }

View File

@@ -3,6 +3,7 @@ package services
import ( import (
"net/http" "net/http"
"strconv" "strconv"
"time"
"wm-backend/global" "wm-backend/global"
"wm-backend/internal/repositories" "wm-backend/internal/repositories"
"wm-backend/response" "wm-backend/response"
@@ -91,3 +92,57 @@ func DashboardAnomalies(c *gin.Context) error {
response.Ok(c, "Success", anomalies) response.Ok(c, "Success", anomalies)
return nil return nil
} }
// @Summary Get transactions chart data
// @Description Retrieve import/export transaction quantities grouped by date for chart display
// @Tags dashboard
// @Accept json
// @Produce json
// @Param period query string false "Time period: today, 7d, 30d, this_month" default(7d)
// @Param warehouse_id query int false "Filter by warehouse ID"
// @Success 200 {object} response.SuccessResponse{data=models.TransactionChartData}
// @Failure 400 {object} response.ErrorResponse
// @Failure 500 {object} response.ErrorResponse
// @Router /v1/dashboard/transactions-chart [get]
func DashboardTransactionsChart(c *gin.Context) error {
period := c.DefaultQuery("period", "7d")
now := time.Now()
var startDate, endDate time.Time
switch period {
case "today":
startDate = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
endDate = startDate.AddDate(0, 0, 1)
case "7d":
startDate = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, -6)
endDate = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, 1)
case "30d":
startDate = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, -29)
endDate = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, 1)
case "this_month":
startDate = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
endDate = startDate.AddDate(0, 1, 0)
default:
response.BadRequestError(c, http.StatusBadRequest, "Invalid period. Use: today, 7d, 30d, this_month")
return nil
}
var warehouseID pgtype.Int8
if raw := c.Query("warehouse_id"); raw != "" {
id, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
response.BadRequestError(c, http.StatusBadRequest, "Invalid warehouse_id")
return nil
}
warehouseID = pgtype.Int8{Int64: id, Valid: true}
}
data, err := repositories.GetTransactionChartData(c.Request.Context(), global.Queries, startDate, endDate, warehouseID)
if err != nil {
log.Err(err).Msg("Error when Get Transaction Chart Data")
response.InternalServerError(c, http.StatusInternalServerError, "Failed to get transaction chart data")
return nil
}
response.Ok(c, "Success", data)
return nil
}

View File

@@ -253,3 +253,50 @@ func (q *Queries) GetTotalComponentStats(ctx context.Context, warehouseID pgtype
err := row.Scan(&i.TotalTypes, &i.TotalQuantity) err := row.Scan(&i.TotalTypes, &i.TotalQuantity)
return i, err return i, err
} }
const getTransactionChartData = `-- name: GetTransactionChartData :many
SELECT DATE(st.created_at) AS date, st.transaction_type, COALESCE(SUM(st.quantity), 0)::bigint AS total_quantity
FROM stock_transactions st
JOIN containers con ON st.container_id = con.id
JOIN shelves s ON con.shelf_id = s.id
JOIN cabinets cab ON s.cabinet_id = cab.id
JOIN rooms r ON cab.room_id = r.id
WHERE st.transaction_type IN ('import', 'export')
AND st.created_at >= $1::timestamptz
AND st.created_at < $2::timestamptz
AND ($3::bigint IS NULL OR r.warehouse_id = $3::bigint)
GROUP BY DATE(st.created_at), st.transaction_type
ORDER BY DATE(st.created_at) ASC
`
type GetTransactionChartDataParams struct {
StartDate time.Time `db:"start_date" json:"startDate"`
EndDate time.Time `db:"end_date" json:"endDate"`
WarehouseID pgtype.Int8 `db:"warehouse_id" json:"warehouseId"`
}
type GetTransactionChartDataRow struct {
Date pgtype.Date `db:"date" json:"date"`
TransactionType TransactionTypeEnum `db:"transaction_type" json:"transactionType"`
TotalQuantity int64 `db:"total_quantity" json:"totalQuantity"`
}
func (q *Queries) GetTransactionChartData(ctx context.Context, arg GetTransactionChartDataParams) ([]GetTransactionChartDataRow, error) {
rows, err := q.db.Query(ctx, getTransactionChartData, arg.StartDate, arg.EndDate, arg.WarehouseID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTransactionChartDataRow
for rows.Next() {
var i GetTransactionChartDataRow
if err := rows.Scan(&i.Date, &i.TransactionType, &i.TotalQuantity); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@@ -71,6 +71,7 @@ type Querier interface {
GetStockAlerts(ctx context.Context) ([]GetStockAlertsRow, error) GetStockAlerts(ctx context.Context) ([]GetStockAlertsRow, error)
GetTodayInvoiceCounts(ctx context.Context) ([]GetTodayInvoiceCountsRow, error) GetTodayInvoiceCounts(ctx context.Context) ([]GetTodayInvoiceCountsRow, error)
GetTotalComponentStats(ctx context.Context, warehouseID pgtype.Int8) (GetTotalComponentStatsRow, error) GetTotalComponentStats(ctx context.Context, warehouseID pgtype.Int8) (GetTotalComponentStatsRow, error)
GetTransactionChartData(ctx context.Context, arg GetTransactionChartDataParams) ([]GetTransactionChartDataRow, error)
GetUserByEmail(ctx context.Context, email string) (User, error) GetUserByEmail(ctx context.Context, email string) (User, error)
GetUserByID(ctx context.Context, id uuid.UUID) (User, error) GetUserByID(ctx context.Context, id uuid.UUID) (User, error)
GetUserByUsername(ctx context.Context, username string) (User, error) GetUserByUsername(ctx context.Context, username string) (User, error)