feat: add endpoints for retrieving stock alerts and anomaly items, including database queries and models

This commit is contained in:
Tran Anh Tuan
2026-05-13 18:10:34 +07:00
parent 383bed757d
commit 0a56dfeb61
11 changed files with 695 additions and 0 deletions

View File

@@ -42,3 +42,24 @@ JOIN cabinets cab ON s.cabinet_id = cab.id
JOIN rooms r ON cab.room_id = r.id JOIN rooms r ON cab.room_id = r.id
LEFT JOIN component_items ci ON c.id = ci.container_id LEFT JOIN component_items ci ON c.id = ci.container_id
WHERE sqlc.narg('warehouse_id')::bigint IS NULL OR r.warehouse_id = sqlc.narg('warehouse_id')::bigint; WHERE sqlc.narg('warehouse_id')::bigint IS NULL OR r.warehouse_id = sqlc.narg('warehouse_id')::bigint;
-- name: GetStockAlerts :many
SELECT c.id, c.name, c.unit, c.total_quantity, c.min_quantity, c.component_type_id,
ct.name AS component_type_name
FROM components c
LEFT JOIN component_types ct ON c.component_type_id = ct.id
WHERE c.total_quantity <= c.min_quantity
ORDER BY (c.total_quantity - c.min_quantity) ASC;
-- name: GetAnomalyItems :many
SELECT ci.id, ci.component_id, ci.container_id, ci.quantity, ci.status, ci.created_at, ci.updated_at,
c.name AS component_name, c.unit AS component_unit
FROM component_items ci
JOIN components c ON ci.component_id = c.id
JOIN containers con ON ci.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 ci.status != 'normal'
AND (sqlc.narg('warehouse_id')::bigint IS NULL OR r.warehouse_id = sqlc.narg('warehouse_id')::bigint)
ORDER BY ci.updated_at DESC;

View File

@@ -2189,6 +2189,108 @@ const docTemplate = `{
} }
} }
}, },
"/v1/dashboard/anomalies": {
"get": {
"description": "Retrieve list of component items with abnormal status (damaged, expired, long_unused, pending_inspection)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"dashboard"
],
"summary": "Get anomaly items",
"parameters": [
{
"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": {
"type": "array",
"items": {
"$ref": "#/definitions/models.AnomalyItem"
}
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/v1/dashboard/stock-alerts": {
"get": {
"description": "Retrieve list of components that are low on stock (total_quantity \u003c= min_quantity)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"dashboard"
],
"summary": "Get stock alerts",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/models.StockAlert"
}
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/v1/dashboard/summary": { "/v1/dashboard/summary": {
"get": { "get": {
"description": "Retrieve dashboard summary with key statistics", "description": "Retrieve dashboard summary with key statistics",
@@ -2202,6 +2304,14 @@ const docTemplate = `{
"dashboard" "dashboard"
], ],
"summary": "Get dashboard summary", "summary": "Get dashboard summary",
"parameters": [
{
"type": "integer",
"description": "Filter by warehouse ID",
"name": "warehouse_id",
"in": "query"
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@@ -3904,6 +4014,38 @@ const docTemplate = `{
} }
} }
}, },
"models.AnomalyItem": {
"type": "object",
"properties": {
"componentId": {
"type": "integer"
},
"componentName": {
"type": "string"
},
"componentUnit": {
"type": "string"
},
"containerId": {
"type": "integer"
},
"createdAt": {
"type": "string"
},
"id": {
"type": "integer"
},
"quantity": {
"type": "integer"
},
"status": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
"models.Cabinet": { "models.Cabinet": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -4322,6 +4464,32 @@ const docTemplate = `{
} }
} }
}, },
"models.StockAlert": {
"type": "object",
"properties": {
"componentTypeId": {
"type": "integer"
},
"componentTypeName": {
"type": "string"
},
"id": {
"type": "integer"
},
"minQuantity": {
"type": "integer"
},
"name": {
"type": "string"
},
"totalQuantity": {
"type": "integer"
},
"unit": {
"type": "string"
}
}
},
"models.TodayInvoiceCount": { "models.TodayInvoiceCount": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -2183,6 +2183,108 @@
} }
} }
}, },
"/v1/dashboard/anomalies": {
"get": {
"description": "Retrieve list of component items with abnormal status (damaged, expired, long_unused, pending_inspection)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"dashboard"
],
"summary": "Get anomaly items",
"parameters": [
{
"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": {
"type": "array",
"items": {
"$ref": "#/definitions/models.AnomalyItem"
}
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/v1/dashboard/stock-alerts": {
"get": {
"description": "Retrieve list of components that are low on stock (total_quantity \u003c= min_quantity)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"dashboard"
],
"summary": "Get stock alerts",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/models.StockAlert"
}
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/v1/dashboard/summary": { "/v1/dashboard/summary": {
"get": { "get": {
"description": "Retrieve dashboard summary with key statistics", "description": "Retrieve dashboard summary with key statistics",
@@ -2196,6 +2298,14 @@
"dashboard" "dashboard"
], ],
"summary": "Get dashboard summary", "summary": "Get dashboard summary",
"parameters": [
{
"type": "integer",
"description": "Filter by warehouse ID",
"name": "warehouse_id",
"in": "query"
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@@ -3898,6 +4008,38 @@
} }
} }
}, },
"models.AnomalyItem": {
"type": "object",
"properties": {
"componentId": {
"type": "integer"
},
"componentName": {
"type": "string"
},
"componentUnit": {
"type": "string"
},
"containerId": {
"type": "integer"
},
"createdAt": {
"type": "string"
},
"id": {
"type": "integer"
},
"quantity": {
"type": "integer"
},
"status": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
"models.Cabinet": { "models.Cabinet": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -4316,6 +4458,32 @@
} }
} }
}, },
"models.StockAlert": {
"type": "object",
"properties": {
"componentTypeId": {
"type": "integer"
},
"componentTypeName": {
"type": "string"
},
"id": {
"type": "integer"
},
"minQuantity": {
"type": "integer"
},
"name": {
"type": "string"
},
"totalQuantity": {
"type": "integer"
},
"unit": {
"type": "string"
}
}
},
"models.TodayInvoiceCount": { "models.TodayInvoiceCount": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -26,6 +26,27 @@ definitions:
priority: priority:
type: integer type: integer
type: object type: object
models.AnomalyItem:
properties:
componentId:
type: integer
componentName:
type: string
componentUnit:
type: string
containerId:
type: integer
createdAt:
type: string
id:
type: integer
quantity:
type: integer
status:
type: string
updatedAt:
type: string
type: object
models.Cabinet: models.Cabinet:
properties: properties:
createdAt: createdAt:
@@ -300,6 +321,23 @@ definitions:
updatedAt: updatedAt:
type: string type: string
type: object type: object
models.StockAlert:
properties:
componentTypeId:
type: integer
componentTypeName:
type: string
id:
type: integer
minQuantity:
type: integer
name:
type: string
totalQuantity:
type: integer
unit:
type: string
type: object
models.TodayInvoiceCount: models.TodayInvoiceCount:
properties: properties:
count: count:
@@ -2416,11 +2454,79 @@ paths:
summary: Update container summary: Update container
tags: tags:
- container - container
/v1/dashboard/anomalies:
get:
consumes:
- application/json
description: Retrieve list of component items with abnormal status (damaged,
expired, long_unused, pending_inspection)
parameters:
- 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:
items:
$ref: '#/definitions/models.AnomalyItem'
type: array
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: Get anomaly items
tags:
- dashboard
/v1/dashboard/stock-alerts:
get:
consumes:
- application/json
description: Retrieve list of components that are low on stock (total_quantity
<= min_quantity)
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.SuccessResponse'
- properties:
data:
items:
$ref: '#/definitions/models.StockAlert'
type: array
type: object
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: Get stock alerts
tags:
- dashboard
/v1/dashboard/summary: /v1/dashboard/summary:
get: get:
consumes: consumes:
- application/json - application/json
description: Retrieve dashboard summary with key statistics description: Retrieve dashboard summary with key statistics
parameters:
- description: Filter by warehouse ID
in: query
name: warehouse_id
type: integer
produces: produces:
- application/json - application/json
responses: responses:

View File

@@ -32,3 +32,29 @@ func ToDomainContainerStats(r db.GetContainerStatsRow) models.ContainerStats {
EmptyContainers: int64(r.EmptyContainers), EmptyContainers: int64(r.EmptyContainers),
} }
} }
func ToDomainStockAlert(r db.GetStockAlertsRow) models.StockAlert {
return models.StockAlert{
ID: r.ID,
Name: r.Name,
Unit: r.Unit,
TotalQuantity: r.TotalQuantity,
MinQuantity: r.MinQuantity,
ComponentTypeID: r.ComponentTypeID,
ComponentTypeName: r.ComponentTypeName.String,
}
}
func ToDomainAnomalyItem(r db.GetAnomalyItemsRow) models.AnomalyItem {
return models.AnomalyItem{
ID: r.ID,
ComponentID: r.ComponentID,
ContainerID: r.ContainerID,
Quantity: r.Quantity,
Status: string(r.Status),
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
ComponentName: r.ComponentName,
ComponentUnit: r.ComponentUnit,
}
}

View File

@@ -1,5 +1,7 @@
package models package models
import "time"
type TotalComponentStats struct { type TotalComponentStats struct {
TotalTypes int64 `json:"totalTypes"` TotalTypes int64 `json:"totalTypes"`
TotalQuantity int64 `json:"totalQuantity"` TotalQuantity int64 `json:"totalQuantity"`
@@ -28,3 +30,25 @@ type DashboardSummary struct {
TodayInvoices []TodayInvoiceCount `json:"todayInvoices"` TodayInvoices []TodayInvoiceCount `json:"todayInvoices"`
EmptyContainers ContainerStats `json:"emptyContainers"` EmptyContainers ContainerStats `json:"emptyContainers"`
} }
type StockAlert struct {
ID int64 `json:"id"`
Name string `json:"name"`
Unit string `json:"unit"`
TotalQuantity int32 `json:"totalQuantity"`
MinQuantity int32 `json:"minQuantity"`
ComponentTypeID int64 `json:"componentTypeId"`
ComponentTypeName string `json:"componentTypeName"`
}
type AnomalyItem struct {
ID int64 `json:"id"`
ComponentID int64 `json:"componentId"`
ContainerID int64 `json:"containerId"`
Quantity int32 `json:"quantity"`
Status string `json:"status"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ComponentName string `json:"componentName"`
ComponentUnit string `json:"componentUnit"`
}

View File

@@ -59,3 +59,27 @@ func GetDashboardSummary(ctx context.Context, queries *db.Queries, warehouseID p
EmptyContainers: mapper.ToDomainContainerStats(containerStats), EmptyContainers: mapper.ToDomainContainerStats(containerStats),
}, nil }, nil
} }
func GetStockAlerts(ctx context.Context, queries *db.Queries) ([]models.StockAlert, error) {
results, err := queries.GetStockAlerts(ctx)
if err != nil {
return nil, err
}
items := make([]models.StockAlert, 0, len(results))
for _, r := range results {
items = append(items, mapper.ToDomainStockAlert(r))
}
return items, nil
}
func GetAnomalyItems(ctx context.Context, queries *db.Queries, warehouseID pgtype.Int8) ([]models.AnomalyItem, error) {
results, err := queries.GetAnomalyItems(ctx, warehouseID)
if err != nil {
return nil, err
}
items := make([]models.AnomalyItem, 0, len(results))
for _, r := range results {
items = append(items, mapper.ToDomainAnomalyItem(r))
}
return items, nil
}

View File

@@ -156,6 +156,8 @@ func NewRouter() *gin.Engine {
dashboard := protected.Group(constants.API_GROUP_DASHBOARD) dashboard := protected.Group(constants.API_GROUP_DASHBOARD)
{ {
dashboard.GET("/summary", utils.AsyncHandler(services.DashboardSummary)) dashboard.GET("/summary", utils.AsyncHandler(services.DashboardSummary))
dashboard.GET("/stock-alerts", utils.AsyncHandler(services.DashboardStockAlerts))
dashboard.GET("/anomalies", utils.AsyncHandler(services.DashboardAnomalies))
} }
} }
} }

View File

@@ -41,3 +41,53 @@ func DashboardSummary(c *gin.Context) error {
response.Ok(c, "Success", summary) response.Ok(c, "Success", summary)
return nil return nil
} }
// @Summary Get stock alerts
// @Description Retrieve list of components that are low on stock (total_quantity <= min_quantity)
// @Tags dashboard
// @Accept json
// @Produce json
// @Success 200 {object} response.SuccessResponse{data=[]models.StockAlert}
// @Failure 500 {object} response.ErrorResponse
// @Router /v1/dashboard/stock-alerts [get]
func DashboardStockAlerts(c *gin.Context) error {
alerts, err := repositories.GetStockAlerts(c.Request.Context(), global.Queries)
if err != nil {
log.Err(err).Msg("Error when Get Stock Alerts")
response.InternalServerError(c, http.StatusInternalServerError, "Failed to get stock alerts")
return nil
}
response.Ok(c, "Success", alerts)
return nil
}
// @Summary Get anomaly items
// @Description Retrieve list of component items with abnormal status (damaged, expired, long_unused, pending_inspection)
// @Tags dashboard
// @Accept json
// @Produce json
// @Param warehouse_id query int false "Filter by warehouse ID"
// @Success 200 {object} response.SuccessResponse{data=[]models.AnomalyItem}
// @Failure 400 {object} response.ErrorResponse
// @Failure 500 {object} response.ErrorResponse
// @Router /v1/dashboard/anomalies [get]
func DashboardAnomalies(c *gin.Context) error {
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}
}
anomalies, err := repositories.GetAnomalyItems(c.Request.Context(), global.Queries, warehouseID)
if err != nil {
log.Err(err).Msg("Error when Get Anomaly Items")
response.InternalServerError(c, http.StatusInternalServerError, "Failed to get anomaly items")
return nil
}
response.Ok(c, "Success", anomalies)
return nil
}

View File

@@ -7,6 +7,7 @@ package db
import ( import (
"context" "context"
"time"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
@@ -72,6 +73,62 @@ func (q *Queries) GetAbnormalItemCounts(ctx context.Context, warehouseID pgtype.
return items, nil return items, nil
} }
const getAnomalyItems = `-- name: GetAnomalyItems :many
SELECT ci.id, ci.component_id, ci.container_id, ci.quantity, ci.status, ci.created_at, ci.updated_at,
c.name AS component_name, c.unit AS component_unit
FROM component_items ci
JOIN components c ON ci.component_id = c.id
JOIN containers con ON ci.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 ci.status != 'normal'
AND ($1::bigint IS NULL OR r.warehouse_id = $1::bigint)
ORDER BY ci.updated_at DESC
`
type GetAnomalyItemsRow struct {
ID int64 `db:"id" json:"id"`
ComponentID int64 `db:"component_id" json:"componentId"`
ContainerID int64 `db:"container_id" json:"containerId"`
Quantity int32 `db:"quantity" json:"quantity"`
Status ComponentItemStatusEnum `db:"status" json:"status"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
ComponentName string `db:"component_name" json:"componentName"`
ComponentUnit string `db:"component_unit" json:"componentUnit"`
}
func (q *Queries) GetAnomalyItems(ctx context.Context, warehouseID pgtype.Int8) ([]GetAnomalyItemsRow, error) {
rows, err := q.db.Query(ctx, getAnomalyItems, warehouseID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAnomalyItemsRow
for rows.Next() {
var i GetAnomalyItemsRow
if err := rows.Scan(
&i.ID,
&i.ComponentID,
&i.ContainerID,
&i.Quantity,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
&i.ComponentName,
&i.ComponentUnit,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getContainerStats = `-- name: GetContainerStats :one const getContainerStats = `-- name: GetContainerStats :one
SELECT SELECT
COUNT(*) AS total_containers, COUNT(*) AS total_containers,
@@ -96,6 +153,53 @@ func (q *Queries) GetContainerStats(ctx context.Context, warehouseID pgtype.Int8
return i, err return i, err
} }
const getStockAlerts = `-- name: GetStockAlerts :many
SELECT c.id, c.name, c.unit, c.total_quantity, c.min_quantity, c.component_type_id,
ct.name AS component_type_name
FROM components c
LEFT JOIN component_types ct ON c.component_type_id = ct.id
WHERE c.total_quantity <= c.min_quantity
ORDER BY (c.total_quantity - c.min_quantity) ASC
`
type GetStockAlertsRow struct {
ID int64 `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Unit string `db:"unit" json:"unit"`
TotalQuantity int32 `db:"total_quantity" json:"totalQuantity"`
MinQuantity int32 `db:"min_quantity" json:"minQuantity"`
ComponentTypeID int64 `db:"component_type_id" json:"componentTypeId"`
ComponentTypeName pgtype.Text `db:"component_type_name" json:"componentTypeName"`
}
func (q *Queries) GetStockAlerts(ctx context.Context) ([]GetStockAlertsRow, error) {
rows, err := q.db.Query(ctx, getStockAlerts)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetStockAlertsRow
for rows.Next() {
var i GetStockAlertsRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Unit,
&i.TotalQuantity,
&i.MinQuantity,
&i.ComponentTypeID,
&i.ComponentTypeName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTodayInvoiceCounts = `-- name: GetTodayInvoiceCounts :many const getTodayInvoiceCounts = `-- name: GetTodayInvoiceCounts :many
SELECT type, COUNT(*) AS count SELECT type, COUNT(*) AS count
FROM invoices FROM invoices

View File

@@ -51,6 +51,7 @@ type Querier interface {
FindComponentItem(ctx context.Context, componentid int64) ([]FindComponentItemRow, error) FindComponentItem(ctx context.Context, componentid int64) ([]FindComponentItemRow, error)
GetAbnormalItemCounts(ctx context.Context, warehouseID pgtype.Int8) ([]GetAbnormalItemCountsRow, error) GetAbnormalItemCounts(ctx context.Context, warehouseID pgtype.Int8) ([]GetAbnormalItemCountsRow, error)
GetAlternativeComponentByID(ctx context.Context, id int64) (AlternativeComponent, error) GetAlternativeComponentByID(ctx context.Context, id int64) (AlternativeComponent, error)
GetAnomalyItems(ctx context.Context, warehouseID pgtype.Int8) ([]GetAnomalyItemsRow, error)
GetCabinetByID(ctx context.Context, id int64) (Cabinet, error) GetCabinetByID(ctx context.Context, id int64) (Cabinet, error)
GetComponentByID(ctx context.Context, id int64) (Component, error) GetComponentByID(ctx context.Context, id int64) (Component, error)
GetComponentCodeByID(ctx context.Context, id int64) (ComponentCode, error) GetComponentCodeByID(ctx context.Context, id int64) (ComponentCode, error)
@@ -67,6 +68,7 @@ type Querier interface {
GetRoleByID(ctx context.Context, id uuid.UUID) (Role, error) GetRoleByID(ctx context.Context, id uuid.UUID) (Role, error)
GetRoomByID(ctx context.Context, id int64) (Room, error) GetRoomByID(ctx context.Context, id int64) (Room, error)
GetShelveByID(ctx context.Context, id int64) (Shelf, error) GetShelveByID(ctx context.Context, id int64) (Shelf, 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)
GetUserByEmail(ctx context.Context, email string) (User, error) GetUserByEmail(ctx context.Context, email string) (User, error)