feat: add endpoints for retrieving stock alerts and anomaly items, including database queries and models
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user