diff --git a/db/queries/dashboard.sql b/db/queries/dashboard.sql index b7c94db..052aa03 100644 --- a/db/queries/dashboard.sql +++ b/db/queries/dashboard.sql @@ -95,3 +95,26 @@ WHERE st.transaction_type = 'export' GROUP BY c.id, c.name, c.unit, ct.name ORDER BY total_exported DESC LIMIT sqlc.arg('limit_count')::int; + +-- name: GetStatusDistribution :many +SELECT status, COUNT(*) AS count, COALESCE(SUM(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) +GROUP BY ci.status; + +-- name: GetSpaceUsage :many +SELECT w.name AS warehouse, r.name AS room, + COUNT(DISTINCT c.id)::bigint AS total_containers, + COUNT(DISTINCT ci.container_id)::bigint AS used_containers +FROM warehouses w +JOIN rooms r ON r.warehouse_id = w.id +JOIN cabinets cb ON cb.room_id = r.id +JOIN shelves s ON s.cabinet_id = cb.id +JOIN containers c ON c.shelf_id = s.id +LEFT JOIN component_items ci ON ci.container_id = c.id +WHERE (sqlc.narg('warehouse_id')::bigint IS NULL OR w.id = sqlc.narg('warehouse_id')::bigint) +GROUP BY w.name, r.name; diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index fc97372..be21f9c 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -2247,6 +2247,122 @@ const docTemplate = `{ } } }, + "/v1/dashboard/space-usage": { + "get": { + "description": "Retrieve warehouse space usage statistics showing total vs used containers per room", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "dashboard" + ], + "summary": "Get space usage", + "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.SpaceUsageItem" + } + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + } + }, + "/v1/dashboard/status-distribution": { + "get": { + "description": "Retrieve component items count grouped by status (normal, damaged, expired, etc.)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "dashboard" + ], + "summary": "Get status distribution", + "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.StatusDistributionItem" + } + } + } + } + ] + } + }, + "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)", @@ -4598,6 +4714,37 @@ const docTemplate = `{ } } }, + "models.SpaceUsageItem": { + "type": "object", + "properties": { + "room": { + "type": "string" + }, + "totalContainers": { + "type": "integer" + }, + "usedContainers": { + "type": "integer" + }, + "warehouse": { + "type": "string" + } + } + }, + "models.StatusDistributionItem": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "totalQuantity": { + "type": "integer" + } + } + }, "models.StockAlert": { "type": "object", "properties": { diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index a632fcf..03d3340 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -2241,6 +2241,122 @@ } } }, + "/v1/dashboard/space-usage": { + "get": { + "description": "Retrieve warehouse space usage statistics showing total vs used containers per room", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "dashboard" + ], + "summary": "Get space usage", + "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.SpaceUsageItem" + } + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + } + }, + "/v1/dashboard/status-distribution": { + "get": { + "description": "Retrieve component items count grouped by status (normal, damaged, expired, etc.)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "dashboard" + ], + "summary": "Get status distribution", + "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.StatusDistributionItem" + } + } + } + } + ] + } + }, + "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)", @@ -4592,6 +4708,37 @@ } } }, + "models.SpaceUsageItem": { + "type": "object", + "properties": { + "room": { + "type": "string" + }, + "totalContainers": { + "type": "integer" + }, + "usedContainers": { + "type": "integer" + }, + "warehouse": { + "type": "string" + } + } + }, + "models.StatusDistributionItem": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "totalQuantity": { + "type": "integer" + } + } + }, "models.StockAlert": { "type": "object", "properties": { diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index ac8ed79..79529a9 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -321,6 +321,26 @@ definitions: updatedAt: type: string type: object + models.SpaceUsageItem: + properties: + room: + type: string + totalContainers: + type: integer + usedContainers: + type: integer + warehouse: + type: string + type: object + models.StatusDistributionItem: + properties: + count: + type: integer + status: + type: string + totalQuantity: + type: integer + type: object models.StockAlert: properties: componentTypeId: @@ -2519,6 +2539,78 @@ paths: summary: Get anomaly items tags: - dashboard + /v1/dashboard/space-usage: + get: + consumes: + - application/json + description: Retrieve warehouse space usage statistics showing total vs used + containers per room + 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.SpaceUsageItem' + 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 space usage + tags: + - dashboard + /v1/dashboard/status-distribution: + get: + consumes: + - application/json + description: Retrieve component items count grouped by status (normal, damaged, + expired, etc.) + 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.StatusDistributionItem' + 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 status distribution + tags: + - dashboard /v1/dashboard/stock-alerts: get: consumes: diff --git a/internal/mapper/dashboard_mapper.go b/internal/mapper/dashboard_mapper.go index 4a59d3b..e575550 100644 --- a/internal/mapper/dashboard_mapper.go +++ b/internal/mapper/dashboard_mapper.go @@ -76,3 +76,20 @@ func ToDomainTopExportedComponent(r db.GetTopExportedComponentsRow) models.TopEx TotalExported: r.TotalExported, } } + +func ToDomainStatusDistribution(r db.GetStatusDistributionRow) models.StatusDistributionItem { + return models.StatusDistributionItem{ + Status: string(r.Status), + Count: r.Count, + TotalQuantity: r.TotalQuantity, + } +} + +func ToDomainSpaceUsage(r db.GetSpaceUsageRow) models.SpaceUsageItem { + return models.SpaceUsageItem{ + Warehouse: r.Warehouse, + Room: r.Room, + TotalContainers: r.TotalContainers, + UsedContainers: r.UsedContainers, + } +} diff --git a/internal/models/dashboard_model.go b/internal/models/dashboard_model.go index 979ce32..d1f49d1 100644 --- a/internal/models/dashboard_model.go +++ b/internal/models/dashboard_model.go @@ -76,3 +76,16 @@ type TopExportedComponent struct { ComponentTypeName string `json:"componentTypeName"` TotalExported int64 `json:"totalExported"` } + +type StatusDistributionItem struct { + Status string `json:"status"` + Count int64 `json:"count"` + TotalQuantity int64 `json:"totalQuantity"` +} + +type SpaceUsageItem struct { + Warehouse string `json:"warehouse"` + Room string `json:"room"` + TotalContainers int64 `json:"totalContainers"` + UsedContainers int64 `json:"usedContainers"` +} diff --git a/internal/repositories/dashboard_repository.go b/internal/repositories/dashboard_repository.go index 461ae86..b7e5f95 100644 --- a/internal/repositories/dashboard_repository.go +++ b/internal/repositories/dashboard_repository.go @@ -142,3 +142,27 @@ func GetTopExportedComponents(ctx context.Context, queries *db.Queries, startDat } return items, nil } + +func GetStatusDistribution(ctx context.Context, queries *db.Queries, warehouseID pgtype.Int8) ([]models.StatusDistributionItem, error) { + results, err := queries.GetStatusDistribution(ctx, warehouseID) + if err != nil { + return nil, err + } + items := make([]models.StatusDistributionItem, 0, len(results)) + for _, r := range results { + items = append(items, mapper.ToDomainStatusDistribution(r)) + } + return items, nil +} + +func GetSpaceUsage(ctx context.Context, queries *db.Queries, warehouseID pgtype.Int8) ([]models.SpaceUsageItem, error) { + results, err := queries.GetSpaceUsage(ctx, warehouseID) + if err != nil { + return nil, err + } + items := make([]models.SpaceUsageItem, 0, len(results)) + for _, r := range results { + items = append(items, mapper.ToDomainSpaceUsage(r)) + } + return items, nil +} diff --git a/internal/routers/router.go b/internal/routers/router.go index 06f6242..adc65d0 100644 --- a/internal/routers/router.go +++ b/internal/routers/router.go @@ -160,6 +160,8 @@ func NewRouter() *gin.Engine { dashboard.GET("/anomalies", utils.AsyncHandler(services.DashboardAnomalies)) dashboard.GET("/transactions-chart", utils.AsyncHandler(services.DashboardTransactionsChart)) dashboard.GET("/top-components", utils.AsyncHandler(services.DashboardTopComponents)) + dashboard.GET("/status-distribution", utils.AsyncHandler(services.DashboardStatusDistribution)) + dashboard.GET("/space-usage", utils.AsyncHandler(services.DashboardSpaceUsage)) } } } diff --git a/internal/services/dashboard_service.go b/internal/services/dashboard_service.go index 25a7b3d..ae648b3 100644 --- a/internal/services/dashboard_service.go +++ b/internal/services/dashboard_service.go @@ -211,3 +211,65 @@ func DashboardTopComponents(c *gin.Context) error { response.Ok(c, "Success", data) return nil } + +// @Summary Get status distribution +// @Description Retrieve component items count grouped by status (normal, damaged, expired, etc.) +// @Tags dashboard +// @Accept json +// @Produce json +// @Param warehouse_id query int false "Filter by warehouse ID" +// @Success 200 {object} response.SuccessResponse{data=[]models.StatusDistributionItem} +// @Failure 400 {object} response.ErrorResponse +// @Failure 500 {object} response.ErrorResponse +// @Router /v1/dashboard/status-distribution [get] +func DashboardStatusDistribution(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} + } + + data, err := repositories.GetStatusDistribution(c.Request.Context(), global.Queries, warehouseID) + if err != nil { + log.Err(err).Msg("Error when Get Status Distribution") + response.InternalServerError(c, http.StatusInternalServerError, "Failed to get status distribution") + return nil + } + response.Ok(c, "Success", data) + return nil +} + +// @Summary Get space usage +// @Description Retrieve warehouse space usage statistics showing total vs used containers per room +// @Tags dashboard +// @Accept json +// @Produce json +// @Param warehouse_id query int false "Filter by warehouse ID" +// @Success 200 {object} response.SuccessResponse{data=[]models.SpaceUsageItem} +// @Failure 400 {object} response.ErrorResponse +// @Failure 500 {object} response.ErrorResponse +// @Router /v1/dashboard/space-usage [get] +func DashboardSpaceUsage(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} + } + + data, err := repositories.GetSpaceUsage(c.Request.Context(), global.Queries, warehouseID) + if err != nil { + log.Err(err).Msg("Error when Get Space Usage") + response.InternalServerError(c, http.StatusInternalServerError, "Failed to get space usage") + return nil + } + response.Ok(c, "Success", data) + return nil +} diff --git a/sqlc_gen/dashboard.sql.go b/sqlc_gen/dashboard.sql.go index cb7c701..9bcd66e 100644 --- a/sqlc_gen/dashboard.sql.go +++ b/sqlc_gen/dashboard.sql.go @@ -153,6 +153,89 @@ func (q *Queries) GetContainerStats(ctx context.Context, warehouseID pgtype.Int8 return i, err } +const getSpaceUsage = `-- name: GetSpaceUsage :many +SELECT w.name AS warehouse, r.name AS room, + COUNT(DISTINCT c.id)::bigint AS total_containers, + COUNT(DISTINCT ci.container_id)::bigint AS used_containers +FROM warehouses w +JOIN rooms r ON r.warehouse_id = w.id +JOIN cabinets cb ON cb.room_id = r.id +JOIN shelves s ON s.cabinet_id = cb.id +JOIN containers c ON c.shelf_id = s.id +LEFT JOIN component_items ci ON ci.container_id = c.id +WHERE ($1::bigint IS NULL OR w.id = $1::bigint) +GROUP BY w.name, r.name +` + +type GetSpaceUsageRow struct { + Warehouse string `db:"warehouse" json:"warehouse"` + Room string `db:"room" json:"room"` + TotalContainers int64 `db:"total_containers" json:"totalContainers"` + UsedContainers int64 `db:"used_containers" json:"usedContainers"` +} + +func (q *Queries) GetSpaceUsage(ctx context.Context, warehouseID pgtype.Int8) ([]GetSpaceUsageRow, error) { + rows, err := q.db.Query(ctx, getSpaceUsage, warehouseID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetSpaceUsageRow + for rows.Next() { + var i GetSpaceUsageRow + if err := rows.Scan( + &i.Warehouse, + &i.Room, + &i.TotalContainers, + &i.UsedContainers, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getStatusDistribution = `-- name: GetStatusDistribution :many +SELECT status, COUNT(*) AS count, COALESCE(SUM(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) +GROUP BY ci.status +` + +type GetStatusDistributionRow struct { + Status ComponentItemStatusEnum `db:"status" json:"status"` + Count int64 `db:"count" json:"count"` + TotalQuantity int64 `db:"total_quantity" json:"totalQuantity"` +} + +func (q *Queries) GetStatusDistribution(ctx context.Context, warehouseID pgtype.Int8) ([]GetStatusDistributionRow, error) { + rows, err := q.db.Query(ctx, getStatusDistribution, warehouseID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetStatusDistributionRow + for rows.Next() { + var i GetStatusDistributionRow + if err := rows.Scan(&i.Status, &i.Count, &i.TotalQuantity); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + 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 diff --git a/sqlc_gen/querier.go b/sqlc_gen/querier.go index bdbb017..649bb16 100644 --- a/sqlc_gen/querier.go +++ b/sqlc_gen/querier.go @@ -68,6 +68,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) + GetSpaceUsage(ctx context.Context, warehouseID pgtype.Int8) ([]GetSpaceUsageRow, error) + GetStatusDistribution(ctx context.Context, warehouseID pgtype.Int8) ([]GetStatusDistributionRow, error) GetStockAlerts(ctx context.Context) ([]GetStockAlertsRow, error) GetTodayInvoiceCounts(ctx context.Context) ([]GetTodayInvoiceCountsRow, error) GetTopExportedComponents(ctx context.Context, arg GetTopExportedComponentsParams) ([]GetTopExportedComponentsRow, error)