From 383bed757d985fc031235391c573feb7fe670ce8 Mon Sep 17 00:00:00 2001 From: Tran Anh Tuan Date: Wed, 13 May 2026 17:53:32 +0700 Subject: [PATCH] feat: add dashboard summary endpoint and related models, queries, and services --- configs/constants/constants.go | 1 + db/queries/dashboard.sql | 44 ++++ docs/swagger/docs.go | 211 ++++++++++++++++++ docs/swagger/swagger.json | 211 ++++++++++++++++++ docs/swagger/swagger.yaml | 130 +++++++++++ internal/mapper/dashboard_mapper.go | 34 +++ internal/models/dashboard_model.go | 30 +++ internal/repositories/dashboard_repository.go | 61 +++++ internal/routers/router.go | 5 + internal/services/dashboard_service.go | 43 ++++ sqlc_gen/dashboard.sql.go | 151 +++++++++++++ sqlc_gen/querier.go | 7 + 12 files changed, 928 insertions(+) create mode 100644 db/queries/dashboard.sql create mode 100644 internal/mapper/dashboard_mapper.go create mode 100644 internal/models/dashboard_model.go create mode 100644 internal/repositories/dashboard_repository.go create mode 100644 internal/services/dashboard_service.go create mode 100644 sqlc_gen/dashboard.sql.go diff --git a/configs/constants/constants.go b/configs/constants/constants.go index 8f052e1..835b206 100644 --- a/configs/constants/constants.go +++ b/configs/constants/constants.go @@ -24,6 +24,7 @@ const ( API_GROUP_INVOICE_CONFIG_ITEM = "/invoice-config-items" API_GROUP_INVOICE = "/invoices" API_GROUP_ALTERNATIVE_COMPONENT = "/alternative-components" + API_GROUP_DASHBOARD = "/dashboard" ) const ( diff --git a/db/queries/dashboard.sql b/db/queries/dashboard.sql new file mode 100644 index 0000000..ceee7c3 --- /dev/null +++ b/db/queries/dashboard.sql @@ -0,0 +1,44 @@ +-- name: GetTotalComponentStats :one +SELECT COUNT(DISTINCT ci.component_id) AS total_types, COALESCE(SUM(ci.quantity), 0)::bigint AS total_quantity +FROM component_items ci +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 sqlc.narg('warehouse_id')::bigint IS NULL OR r.warehouse_id = sqlc.narg('warehouse_id')::bigint; + +-- name: CountPendingInvoices :one +SELECT COUNT(*) FROM invoices +WHERE status IN ('draft', 'pending'); + +-- name: CountLowStockComponents :one +SELECT COUNT(*) FROM components +WHERE total_quantity <= min_quantity; + +-- name: GetAbnormalItemCounts :many +SELECT ci.status, COUNT(*) AS count +FROM component_items ci +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) +GROUP BY ci.status; + +-- name: GetTodayInvoiceCounts :many +SELECT type, COUNT(*) AS count +FROM invoices +WHERE created_at::date = CURRENT_DATE +GROUP BY type; + +-- name: GetContainerStats :one +SELECT + COUNT(*) AS total_containers, + COUNT(*) - COUNT(DISTINCT ci.container_id) AS empty_containers +FROM containers c +JOIN shelves s ON c.shelf_id = s.id +JOIN cabinets cab ON s.cabinet_id = cab.id +JOIN rooms r ON cab.room_id = r.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; diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index d365cad..6999fb2 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -1327,6 +1327,49 @@ const docTemplate = `{ } } }, + "/profile": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns user info with roles and permissions (requires auth)", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Get current user profile", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/responses.BodyProfileResponse" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + } + }, "/v1/alternative-components": { "get": { "description": "Retrieve a list of all alternative components", @@ -2146,6 +2189,47 @@ const docTemplate = `{ } } }, + "/v1/dashboard/summary": { + "get": { + "description": "Retrieve dashboard summary with key statistics", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "dashboard" + ], + "summary": "Get dashboard summary", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.DashboardSummary" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + } + }, "/v1/invoice-config-items": { "get": { "description": "Retrieve a list of all invoice config items", @@ -3780,6 +3864,17 @@ const docTemplate = `{ } }, "definitions": { + "models.AbnormalAlert": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "status": { + "type": "string" + } + } + }, "models.AlternativeComponent": { "type": "object", "properties": { @@ -3992,6 +4087,46 @@ const docTemplate = `{ } } }, + "models.ContainerStats": { + "type": "object", + "properties": { + "emptyContainers": { + "type": "integer" + }, + "totalContainers": { + "type": "integer" + } + } + }, + "models.DashboardSummary": { + "type": "object", + "properties": { + "abnormalAlerts": { + "type": "array", + "items": { + "$ref": "#/definitions/models.AbnormalAlert" + } + }, + "emptyContainers": { + "$ref": "#/definitions/models.ContainerStats" + }, + "lowStockComponents": { + "type": "integer" + }, + "pendingInvoices": { + "type": "integer" + }, + "todayInvoices": { + "type": "array", + "items": { + "$ref": "#/definitions/models.TodayInvoiceCount" + } + }, + "totalComponents": { + "$ref": "#/definitions/models.TotalComponentStats" + } + } + }, "models.FindComponentItemResult": { "type": "object", "properties": { @@ -4187,6 +4322,28 @@ const docTemplate = `{ } } }, + "models.TodayInvoiceCount": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + }, + "models.TotalComponentStats": { + "type": "object", + "properties": { + "totalQuantity": { + "type": "integer" + }, + "totalTypes": { + "type": "integer" + } + } + }, "models.Warehouse": { "type": "object", "properties": { @@ -4841,6 +4998,26 @@ const docTemplate = `{ } } }, + "responses.BodyProfileResponse": { + "type": "object", + "properties": { + "info": { + "$ref": "#/definitions/responses.UserInfoResponse" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "roles": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.RoleItem" + } + } + } + }, "responses.BodyRegisterResponse": { "type": "object", "properties": { @@ -4956,6 +5133,20 @@ const docTemplate = `{ } } }, + "responses.RoleItem": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "responses.UpdateAlternativeComponentResponse": { "type": "object", "properties": { @@ -5259,6 +5450,26 @@ const docTemplate = `{ "type": "string" } } + }, + "responses.UserInfoResponse": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "fullName": { + "type": "string" + }, + "id": { + "type": "string" + }, + "isActive": { + "type": "boolean" + }, + "username": { + "type": "string" + } + } } } }` diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 39a7640..a32f35d 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -1321,6 +1321,49 @@ } } }, + "/profile": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns user info with roles and permissions (requires auth)", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Get current user profile", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/responses.BodyProfileResponse" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + } + }, "/v1/alternative-components": { "get": { "description": "Retrieve a list of all alternative components", @@ -2140,6 +2183,47 @@ } } }, + "/v1/dashboard/summary": { + "get": { + "description": "Retrieve dashboard summary with key statistics", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "dashboard" + ], + "summary": "Get dashboard summary", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.DashboardSummary" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + } + }, "/v1/invoice-config-items": { "get": { "description": "Retrieve a list of all invoice config items", @@ -3774,6 +3858,17 @@ } }, "definitions": { + "models.AbnormalAlert": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "status": { + "type": "string" + } + } + }, "models.AlternativeComponent": { "type": "object", "properties": { @@ -3986,6 +4081,46 @@ } } }, + "models.ContainerStats": { + "type": "object", + "properties": { + "emptyContainers": { + "type": "integer" + }, + "totalContainers": { + "type": "integer" + } + } + }, + "models.DashboardSummary": { + "type": "object", + "properties": { + "abnormalAlerts": { + "type": "array", + "items": { + "$ref": "#/definitions/models.AbnormalAlert" + } + }, + "emptyContainers": { + "$ref": "#/definitions/models.ContainerStats" + }, + "lowStockComponents": { + "type": "integer" + }, + "pendingInvoices": { + "type": "integer" + }, + "todayInvoices": { + "type": "array", + "items": { + "$ref": "#/definitions/models.TodayInvoiceCount" + } + }, + "totalComponents": { + "$ref": "#/definitions/models.TotalComponentStats" + } + } + }, "models.FindComponentItemResult": { "type": "object", "properties": { @@ -4181,6 +4316,28 @@ } } }, + "models.TodayInvoiceCount": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + }, + "models.TotalComponentStats": { + "type": "object", + "properties": { + "totalQuantity": { + "type": "integer" + }, + "totalTypes": { + "type": "integer" + } + } + }, "models.Warehouse": { "type": "object", "properties": { @@ -4835,6 +4992,26 @@ } } }, + "responses.BodyProfileResponse": { + "type": "object", + "properties": { + "info": { + "$ref": "#/definitions/responses.UserInfoResponse" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "roles": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.RoleItem" + } + } + } + }, "responses.BodyRegisterResponse": { "type": "object", "properties": { @@ -4950,6 +5127,20 @@ } } }, + "responses.RoleItem": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "responses.UpdateAlternativeComponentResponse": { "type": "object", "properties": { @@ -5253,6 +5444,26 @@ "type": "string" } } + }, + "responses.UserInfoResponse": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "fullName": { + "type": "string" + }, + "id": { + "type": "string" + }, + "isActive": { + "type": "boolean" + }, + "username": { + "type": "string" + } + } } } } \ No newline at end of file diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 84229f0..964f0d1 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -1,5 +1,12 @@ basePath: /api/v1 definitions: + models.AbnormalAlert: + properties: + count: + type: integer + status: + type: string + type: object models.AlternativeComponent: properties: alternativeComponentId: @@ -139,6 +146,32 @@ definitions: updatedAt: type: string type: object + models.ContainerStats: + properties: + emptyContainers: + type: integer + totalContainers: + type: integer + type: object + models.DashboardSummary: + properties: + abnormalAlerts: + items: + $ref: '#/definitions/models.AbnormalAlert' + type: array + emptyContainers: + $ref: '#/definitions/models.ContainerStats' + lowStockComponents: + type: integer + pendingInvoices: + type: integer + todayInvoices: + items: + $ref: '#/definitions/models.TodayInvoiceCount' + type: array + totalComponents: + $ref: '#/definitions/models.TotalComponentStats' + type: object models.FindComponentItemResult: properties: cabinetName: @@ -267,6 +300,20 @@ definitions: updatedAt: type: string type: object + models.TodayInvoiceCount: + properties: + count: + type: integer + type: + type: string + type: object + models.TotalComponentStats: + properties: + totalQuantity: + type: integer + totalTypes: + type: integer + type: object models.Warehouse: properties: address: @@ -703,6 +750,19 @@ definitions: status: type: integer type: object + responses.BodyProfileResponse: + properties: + info: + $ref: '#/definitions/responses.UserInfoResponse' + permissions: + items: + type: string + type: array + roles: + items: + $ref: '#/definitions/responses.RoleItem' + type: array + type: object responses.BodyRegisterResponse: properties: id: @@ -775,6 +835,15 @@ definitions: id: type: integer type: object + responses.RoleItem: + properties: + description: + type: string + id: + type: string + name: + type: string + type: object responses.UpdateAlternativeComponentResponse: properties: alternativeComponentId: @@ -973,6 +1042,19 @@ definitions: name: type: string type: object + responses.UserInfoResponse: + properties: + email: + type: string + fullName: + type: string + id: + type: string + isActive: + type: boolean + username: + type: string + type: object host: localhost:3000 info: contact: {} @@ -1800,6 +1882,30 @@ paths: summary: Health check tags: - health + /profile: + get: + description: Returns user info with roles and permissions (requires auth) + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.SuccessResponse' + - properties: + data: + $ref: '#/definitions/responses.BodyProfileResponse' + type: object + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.ErrorResponse' + security: + - BearerAuth: [] + summary: Get current user profile + tags: + - auth /v1/alternative-components: get: consumes: @@ -2310,6 +2416,30 @@ paths: summary: Update container tags: - container + /v1/dashboard/summary: + get: + consumes: + - application/json + description: Retrieve dashboard summary with key statistics + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.SuccessResponse' + - properties: + data: + $ref: '#/definitions/models.DashboardSummary' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.ErrorResponse' + summary: Get dashboard summary + tags: + - dashboard /v1/invoice-config-items: get: consumes: diff --git a/internal/mapper/dashboard_mapper.go b/internal/mapper/dashboard_mapper.go new file mode 100644 index 0000000..900874c --- /dev/null +++ b/internal/mapper/dashboard_mapper.go @@ -0,0 +1,34 @@ +package mapper + +import ( + "wm-backend/internal/models" + db "wm-backend/sqlc_gen" +) + +func ToDomainTotalComponentStats(r db.GetTotalComponentStatsRow) models.TotalComponentStats { + return models.TotalComponentStats{ + TotalTypes: r.TotalTypes, + TotalQuantity: r.TotalQuantity, + } +} + +func ToDomainAbnormalAlert(r db.GetAbnormalItemCountsRow) models.AbnormalAlert { + return models.AbnormalAlert{ + Status: string(r.Status), + Count: r.Count, + } +} + +func ToDomainTodayInvoiceCount(r db.GetTodayInvoiceCountsRow) models.TodayInvoiceCount { + return models.TodayInvoiceCount{ + Type: string(r.Type), + Count: r.Count, + } +} + +func ToDomainContainerStats(r db.GetContainerStatsRow) models.ContainerStats { + return models.ContainerStats{ + TotalContainers: r.TotalContainers, + EmptyContainers: int64(r.EmptyContainers), + } +} diff --git a/internal/models/dashboard_model.go b/internal/models/dashboard_model.go new file mode 100644 index 0000000..e3b4eef --- /dev/null +++ b/internal/models/dashboard_model.go @@ -0,0 +1,30 @@ +package models + +type TotalComponentStats struct { + TotalTypes int64 `json:"totalTypes"` + TotalQuantity int64 `json:"totalQuantity"` +} + +type AbnormalAlert struct { + Status string `json:"status"` + Count int64 `json:"count"` +} + +type TodayInvoiceCount struct { + Type string `json:"type"` + Count int64 `json:"count"` +} + +type ContainerStats struct { + TotalContainers int64 `json:"totalContainers"` + EmptyContainers int64 `json:"emptyContainers"` +} + +type DashboardSummary struct { + TotalComponents TotalComponentStats `json:"totalComponents"` + PendingInvoices int64 `json:"pendingInvoices"` + LowStockComponents int64 `json:"lowStockComponents"` + AbnormalAlerts []AbnormalAlert `json:"abnormalAlerts"` + TodayInvoices []TodayInvoiceCount `json:"todayInvoices"` + EmptyContainers ContainerStats `json:"emptyContainers"` +} diff --git a/internal/repositories/dashboard_repository.go b/internal/repositories/dashboard_repository.go new file mode 100644 index 0000000..e08e214 --- /dev/null +++ b/internal/repositories/dashboard_repository.go @@ -0,0 +1,61 @@ +package repositories + +import ( + "context" + "wm-backend/internal/mapper" + "wm-backend/internal/models" + db "wm-backend/sqlc_gen" + + "github.com/jackc/pgx/v5/pgtype" +) + +func GetDashboardSummary(ctx context.Context, queries *db.Queries, warehouseID pgtype.Int8) (models.DashboardSummary, error) { + totalStats, err := queries.GetTotalComponentStats(ctx, warehouseID) + if err != nil { + return models.DashboardSummary{}, err + } + + pendingInvoices, err := queries.CountPendingInvoices(ctx) + if err != nil { + return models.DashboardSummary{}, err + } + + lowStockCount, err := queries.CountLowStockComponents(ctx) + if err != nil { + return models.DashboardSummary{}, err + } + + abnormalRows, err := queries.GetAbnormalItemCounts(ctx, warehouseID) + if err != nil { + return models.DashboardSummary{}, err + } + + todayInvoiceRows, err := queries.GetTodayInvoiceCounts(ctx) + if err != nil { + return models.DashboardSummary{}, err + } + + containerStats, err := queries.GetContainerStats(ctx, warehouseID) + if err != nil { + return models.DashboardSummary{}, err + } + + abnormalAlerts := make([]models.AbnormalAlert, 0, len(abnormalRows)) + for _, r := range abnormalRows { + abnormalAlerts = append(abnormalAlerts, mapper.ToDomainAbnormalAlert(r)) + } + + todayInvoices := make([]models.TodayInvoiceCount, 0, len(todayInvoiceRows)) + for _, r := range todayInvoiceRows { + todayInvoices = append(todayInvoices, mapper.ToDomainTodayInvoiceCount(r)) + } + + return models.DashboardSummary{ + TotalComponents: mapper.ToDomainTotalComponentStats(totalStats), + PendingInvoices: pendingInvoices, + LowStockComponents: lowStockCount, + AbnormalAlerts: abnormalAlerts, + TodayInvoices: todayInvoices, + EmptyContainers: mapper.ToDomainContainerStats(containerStats), + }, nil +} diff --git a/internal/routers/router.go b/internal/routers/router.go index 6dbd93e..fd074f9 100644 --- a/internal/routers/router.go +++ b/internal/routers/router.go @@ -152,6 +152,11 @@ func NewRouter() *gin.Engine { alternativeComponent.PUT("/:id", utils.AsyncHandler(services.AlternativeComponentUpdate)) alternativeComponent.DELETE("/:id", utils.AsyncHandler(services.AlternativeComponentDelete)) } + + dashboard := protected.Group(constants.API_GROUP_DASHBOARD) + { + dashboard.GET("/summary", utils.AsyncHandler(services.DashboardSummary)) + } } } diff --git a/internal/services/dashboard_service.go b/internal/services/dashboard_service.go new file mode 100644 index 0000000..235535b --- /dev/null +++ b/internal/services/dashboard_service.go @@ -0,0 +1,43 @@ +package services + +import ( + "net/http" + "strconv" + "wm-backend/global" + "wm-backend/internal/repositories" + "wm-backend/response" + + "github.com/gin-gonic/gin" + "github.com/jackc/pgx/v5/pgtype" + "github.com/rs/zerolog/log" +) + +// @Summary Get dashboard summary +// @Description Retrieve dashboard summary with key statistics +// @Tags dashboard +// @Accept json +// @Produce json +// @Param warehouse_id query int false "Filter by warehouse ID" +// @Success 200 {object} response.SuccessResponse{data=models.DashboardSummary} +// @Failure 500 {object} response.ErrorResponse +// @Router /v1/dashboard/summary [get] +func DashboardSummary(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} + } + + summary, err := repositories.GetDashboardSummary(c.Request.Context(), global.Queries, warehouseID) + if err != nil { + log.Err(err).Msg("Error when Get Dashboard Summary") + response.InternalServerError(c, http.StatusInternalServerError, "Failed to get dashboard summary") + return nil + } + response.Ok(c, "Success", summary) + return nil +} diff --git a/sqlc_gen/dashboard.sql.go b/sqlc_gen/dashboard.sql.go new file mode 100644 index 0000000..45c9163 --- /dev/null +++ b/sqlc_gen/dashboard.sql.go @@ -0,0 +1,151 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: dashboard.sql + +package db + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const countLowStockComponents = `-- name: CountLowStockComponents :one +SELECT COUNT(*) FROM components +WHERE total_quantity <= min_quantity +` + +func (q *Queries) CountLowStockComponents(ctx context.Context) (int64, error) { + row := q.db.QueryRow(ctx, countLowStockComponents) + var count int64 + err := row.Scan(&count) + return count, err +} + +const countPendingInvoices = `-- name: CountPendingInvoices :one +SELECT COUNT(*) FROM invoices +WHERE status IN ('draft', 'pending') +` + +func (q *Queries) CountPendingInvoices(ctx context.Context) (int64, error) { + row := q.db.QueryRow(ctx, countPendingInvoices) + var count int64 + err := row.Scan(&count) + return count, err +} + +const getAbnormalItemCounts = `-- name: GetAbnormalItemCounts :many +SELECT ci.status, COUNT(*) AS count +FROM component_items ci +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) +GROUP BY ci.status +` + +type GetAbnormalItemCountsRow struct { + Status ComponentItemStatusEnum `db:"status" json:"status"` + Count int64 `db:"count" json:"count"` +} + +func (q *Queries) GetAbnormalItemCounts(ctx context.Context, warehouseID pgtype.Int8) ([]GetAbnormalItemCountsRow, error) { + rows, err := q.db.Query(ctx, getAbnormalItemCounts, warehouseID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAbnormalItemCountsRow + for rows.Next() { + var i GetAbnormalItemCountsRow + if err := rows.Scan(&i.Status, &i.Count); 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 +SELECT + COUNT(*) AS total_containers, + COUNT(*) - COUNT(DISTINCT ci.container_id) AS empty_containers +FROM containers c +JOIN shelves s ON c.shelf_id = s.id +JOIN cabinets cab ON s.cabinet_id = cab.id +JOIN rooms r ON cab.room_id = r.id +LEFT JOIN component_items ci ON c.id = ci.container_id +WHERE $1::bigint IS NULL OR r.warehouse_id = $1::bigint +` + +type GetContainerStatsRow struct { + TotalContainers int64 `db:"total_containers" json:"totalContainers"` + EmptyContainers int32 `db:"empty_containers" json:"emptyContainers"` +} + +func (q *Queries) GetContainerStats(ctx context.Context, warehouseID pgtype.Int8) (GetContainerStatsRow, error) { + row := q.db.QueryRow(ctx, getContainerStats, warehouseID) + var i GetContainerStatsRow + err := row.Scan(&i.TotalContainers, &i.EmptyContainers) + return i, err +} + +const getTodayInvoiceCounts = `-- name: GetTodayInvoiceCounts :many +SELECT type, COUNT(*) AS count +FROM invoices +WHERE created_at::date = CURRENT_DATE +GROUP BY type +` + +type GetTodayInvoiceCountsRow struct { + Type InvoiceTypeEnum `db:"type" json:"type"` + Count int64 `db:"count" json:"count"` +} + +func (q *Queries) GetTodayInvoiceCounts(ctx context.Context) ([]GetTodayInvoiceCountsRow, error) { + rows, err := q.db.Query(ctx, getTodayInvoiceCounts) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetTodayInvoiceCountsRow + for rows.Next() { + var i GetTodayInvoiceCountsRow + if err := rows.Scan(&i.Type, &i.Count); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getTotalComponentStats = `-- name: GetTotalComponentStats :one +SELECT COUNT(DISTINCT ci.component_id) AS total_types, COALESCE(SUM(ci.quantity), 0)::bigint AS total_quantity +FROM component_items ci +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 $1::bigint IS NULL OR r.warehouse_id = $1::bigint +` + +type GetTotalComponentStatsRow struct { + TotalTypes int64 `db:"total_types" json:"totalTypes"` + TotalQuantity int64 `db:"total_quantity" json:"totalQuantity"` +} + +func (q *Queries) GetTotalComponentStats(ctx context.Context, warehouseID pgtype.Int8) (GetTotalComponentStatsRow, error) { + row := q.db.QueryRow(ctx, getTotalComponentStats, warehouseID) + var i GetTotalComponentStatsRow + err := row.Scan(&i.TotalTypes, &i.TotalQuantity) + return i, err +} diff --git a/sqlc_gen/querier.go b/sqlc_gen/querier.go index 5f616a7..4eb1c02 100644 --- a/sqlc_gen/querier.go +++ b/sqlc_gen/querier.go @@ -8,10 +8,13 @@ import ( "context" "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" ) type Querier interface { AssignRoleToUser(ctx context.Context, arg AssignRoleToUserParams) (UserRole, error) + CountLowStockComponents(ctx context.Context) (int64, error) + CountPendingInvoices(ctx context.Context) (int64, error) CountUsersByRoleID(ctx context.Context, roleID uuid.UUID) (int64, error) CreateAlternativeComponent(ctx context.Context, arg CreateAlternativeComponentParams) (AlternativeComponent, error) CreateCabinet(ctx context.Context, arg CreateCabinetParams) (Cabinet, error) @@ -46,6 +49,7 @@ type Querier interface { DeleteShelve(ctx context.Context, id int64) (int64, error) DeleteWarehouse(ctx context.Context, id int64) (int64, error) FindComponentItem(ctx context.Context, componentid int64) ([]FindComponentItemRow, error) + GetAbnormalItemCounts(ctx context.Context, warehouseID pgtype.Int8) ([]GetAbnormalItemCountsRow, error) GetAlternativeComponentByID(ctx context.Context, id int64) (AlternativeComponent, error) GetCabinetByID(ctx context.Context, id int64) (Cabinet, error) GetComponentByID(ctx context.Context, id int64) (Component, error) @@ -54,6 +58,7 @@ type Querier interface { GetComponentItemByID(ctx context.Context, id int64) (ComponentItem, error) GetComponentTypeByID(ctx context.Context, id int64) (ComponentType, error) GetContainerByID(ctx context.Context, id int64) (Container, error) + GetContainerStats(ctx context.Context, warehouseID pgtype.Int8) (GetContainerStatsRow, error) GetInvoiceByID(ctx context.Context, id int64) (Invoice, error) GetInvoiceConfigByID(ctx context.Context, id int64) (InvoiceConfig, error) GetInvoiceConfigItemByID(ctx context.Context, id int64) (InvoiceConfigItem, error) @@ -62,6 +67,8 @@ type Querier interface { GetRoleByID(ctx context.Context, id uuid.UUID) (Role, error) GetRoomByID(ctx context.Context, id int64) (Room, error) GetShelveByID(ctx context.Context, id int64) (Shelf, error) + GetTodayInvoiceCounts(ctx context.Context) ([]GetTodayInvoiceCountsRow, error) + GetTotalComponentStats(ctx context.Context, warehouseID pgtype.Int8) (GetTotalComponentStatsRow, error) GetUserByEmail(ctx context.Context, email string) (User, error) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) GetUserByUsername(ctx context.Context, username string) (User, error)