feat: add dashboard summary endpoint and related models, queries, and services
This commit is contained in:
@@ -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 (
|
||||
|
||||
44
db/queries/dashboard.sql
Normal file
44
db/queries/dashboard.sql
Normal file
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
34
internal/mapper/dashboard_mapper.go
Normal file
34
internal/mapper/dashboard_mapper.go
Normal file
@@ -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),
|
||||
}
|
||||
}
|
||||
30
internal/models/dashboard_model.go
Normal file
30
internal/models/dashboard_model.go
Normal file
@@ -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"`
|
||||
}
|
||||
61
internal/repositories/dashboard_repository.go
Normal file
61
internal/repositories/dashboard_repository.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
43
internal/services/dashboard_service.go
Normal file
43
internal/services/dashboard_service.go
Normal file
@@ -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
|
||||
}
|
||||
151
sqlc_gen/dashboard.sql.go
Normal file
151
sqlc_gen/dashboard.sql.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user