diff --git a/db/queries/dashboard.sql b/db/queries/dashboard.sql index 89e004b..b7c94db 100644 --- a/db/queries/dashboard.sql +++ b/db/queries/dashboard.sql @@ -77,3 +77,21 @@ WHERE st.transaction_type IN ('import', 'export') AND (sqlc.narg('warehouse_id')::bigint IS NULL OR r.warehouse_id = sqlc.narg('warehouse_id')::bigint) GROUP BY DATE(st.created_at), st.transaction_type ORDER BY DATE(st.created_at) ASC; + +-- name: GetTopExportedComponents :many +SELECT c.id, c.name, c.unit, ct.name AS component_type_name, + COALESCE(SUM(st.quantity), 0)::bigint AS total_exported +FROM stock_transactions st +JOIN components c ON c.id = st.component_id +LEFT JOIN component_types ct ON c.component_type_id = ct.id +JOIN containers con ON st.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 st.transaction_type = 'export' + AND st.created_at >= sqlc.arg('start_date')::timestamptz + AND st.created_at < sqlc.arg('end_date')::timestamptz + AND (sqlc.narg('warehouse_id')::bigint IS NULL OR r.warehouse_id = sqlc.narg('warehouse_id')::bigint) +GROUP BY c.id, c.name, c.unit, ct.name +ORDER BY total_exported DESC +LIMIT sqlc.arg('limit_count')::int; diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index b14ec16..fc97372 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -2340,6 +2340,78 @@ const docTemplate = `{ } } }, + "/v1/dashboard/top-components": { + "get": { + "description": "Retrieve top components by export quantity for chart display", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "dashboard" + ], + "summary": "Get top exported components", + "parameters": [ + { + "type": "string", + "default": "30d", + "description": "Time period: today, 7d, 30d, this_month", + "name": "period", + "in": "query" + }, + { + "type": "integer", + "description": "Filter by warehouse ID", + "name": "warehouse_id", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "Number of components to return", + "name": "component_quantities", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.TopExportedComponent" + } + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + } + }, "/v1/dashboard/transactions-chart": { "get": { "description": "Retrieve import/export transaction quantities grouped by date for chart display", @@ -4563,6 +4635,26 @@ const docTemplate = `{ } } }, + "models.TopExportedComponent": { + "type": "object", + "properties": { + "componentTypeName": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "totalExported": { + "type": "integer" + }, + "unit": { + "type": "string" + } + } + }, "models.TotalComponentStats": { "type": "object", "properties": { diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index c886f4c..a632fcf 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -2334,6 +2334,78 @@ } } }, + "/v1/dashboard/top-components": { + "get": { + "description": "Retrieve top components by export quantity for chart display", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "dashboard" + ], + "summary": "Get top exported components", + "parameters": [ + { + "type": "string", + "default": "30d", + "description": "Time period: today, 7d, 30d, this_month", + "name": "period", + "in": "query" + }, + { + "type": "integer", + "description": "Filter by warehouse ID", + "name": "warehouse_id", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "Number of components to return", + "name": "component_quantities", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.TopExportedComponent" + } + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + } + }, "/v1/dashboard/transactions-chart": { "get": { "description": "Retrieve import/export transaction quantities grouped by date for chart display", @@ -4557,6 +4629,26 @@ } } }, + "models.TopExportedComponent": { + "type": "object", + "properties": { + "componentTypeName": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "totalExported": { + "type": "integer" + }, + "unit": { + "type": "string" + } + } + }, "models.TotalComponentStats": { "type": "object", "properties": { diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 0a422c3..ac8ed79 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -345,6 +345,19 @@ definitions: type: type: string type: object + models.TopExportedComponent: + properties: + componentTypeName: + type: string + id: + type: integer + name: + type: string + totalExported: + type: integer + unit: + type: string + type: object models.TotalComponentStats: properties: totalQuantity: @@ -2562,6 +2575,51 @@ paths: summary: Get dashboard summary tags: - dashboard + /v1/dashboard/top-components: + get: + consumes: + - application/json + description: Retrieve top components by export quantity for chart display + parameters: + - default: 30d + description: 'Time period: today, 7d, 30d, this_month' + in: query + name: period + type: string + - description: Filter by warehouse ID + in: query + name: warehouse_id + type: integer + - default: 10 + description: Number of components to return + in: query + name: component_quantities + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.SuccessResponse' + - properties: + data: + items: + $ref: '#/definitions/models.TopExportedComponent' + 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 top exported components + tags: + - dashboard /v1/dashboard/transactions-chart: get: consumes: diff --git a/internal/mapper/dashboard_mapper.go b/internal/mapper/dashboard_mapper.go index f7997a2..4a59d3b 100644 --- a/internal/mapper/dashboard_mapper.go +++ b/internal/mapper/dashboard_mapper.go @@ -66,3 +66,13 @@ func ToDomainTransactionChartRow(r db.GetTransactionChartDataRow) models.Transac TotalQuantity: r.TotalQuantity, } } + +func ToDomainTopExportedComponent(r db.GetTopExportedComponentsRow) models.TopExportedComponent { + return models.TopExportedComponent{ + ID: r.ID, + Name: r.Name, + Unit: r.Unit, + ComponentTypeName: r.ComponentTypeName.String, + TotalExported: r.TotalExported, + } +} diff --git a/internal/models/dashboard_model.go b/internal/models/dashboard_model.go index 08331c4..979ce32 100644 --- a/internal/models/dashboard_model.go +++ b/internal/models/dashboard_model.go @@ -68,3 +68,11 @@ type TransactionChartItem struct { type TransactionChartData struct { Items []TransactionChartItem `json:"items"` } + +type TopExportedComponent struct { + ID int64 `json:"id"` + Name string `json:"name"` + Unit string `json:"unit"` + ComponentTypeName string `json:"componentTypeName"` + TotalExported int64 `json:"totalExported"` +} diff --git a/internal/repositories/dashboard_repository.go b/internal/repositories/dashboard_repository.go index e2fc3f6..461ae86 100644 --- a/internal/repositories/dashboard_repository.go +++ b/internal/repositories/dashboard_repository.go @@ -125,3 +125,20 @@ func GetTransactionChartData(ctx context.Context, queries *db.Queries, startDate return models.TransactionChartData{Items: items}, nil } + +func GetTopExportedComponents(ctx context.Context, queries *db.Queries, startDate, endDate time.Time, warehouseID pgtype.Int8, limitCount int32) ([]models.TopExportedComponent, error) { + results, err := queries.GetTopExportedComponents(ctx, db.GetTopExportedComponentsParams{ + StartDate: startDate, + EndDate: endDate, + WarehouseID: warehouseID, + LimitCount: limitCount, + }) + if err != nil { + return nil, err + } + items := make([]models.TopExportedComponent, 0, len(results)) + for _, r := range results { + items = append(items, mapper.ToDomainTopExportedComponent(r)) + } + return items, nil +} diff --git a/internal/routers/router.go b/internal/routers/router.go index 154b757..06f6242 100644 --- a/internal/routers/router.go +++ b/internal/routers/router.go @@ -159,6 +159,7 @@ func NewRouter() *gin.Engine { dashboard.GET("/stock-alerts", utils.AsyncHandler(services.DashboardStockAlerts)) dashboard.GET("/anomalies", utils.AsyncHandler(services.DashboardAnomalies)) dashboard.GET("/transactions-chart", utils.AsyncHandler(services.DashboardTransactionsChart)) + dashboard.GET("/top-components", utils.AsyncHandler(services.DashboardTopComponents)) } } } diff --git a/internal/services/dashboard_service.go b/internal/services/dashboard_service.go index 6ac3947..25a7b3d 100644 --- a/internal/services/dashboard_service.go +++ b/internal/services/dashboard_service.go @@ -146,3 +146,68 @@ func DashboardTransactionsChart(c *gin.Context) error { response.Ok(c, "Success", data) return nil } + +// @Summary Get top exported components +// @Description Retrieve top components by export quantity for chart display +// @Tags dashboard +// @Accept json +// @Produce json +// @Param period query string false "Time period: today, 7d, 30d, this_month" default(30d) +// @Param warehouse_id query int false "Filter by warehouse ID" +// @Param component_quantities query int false "Number of components to return" default(10) +// @Success 200 {object} response.SuccessResponse{data=[]models.TopExportedComponent} +// @Failure 400 {object} response.ErrorResponse +// @Failure 500 {object} response.ErrorResponse +// @Router /v1/dashboard/top-components [get] +func DashboardTopComponents(c *gin.Context) error { + period := c.DefaultQuery("period", "30d") + + now := time.Now() + var startDate, endDate time.Time + switch period { + case "today": + startDate = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + endDate = startDate.AddDate(0, 0, 1) + case "7d": + startDate = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, -6) + endDate = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, 1) + case "30d": + startDate = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, -29) + endDate = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, 1) + case "this_month": + startDate = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) + endDate = startDate.AddDate(0, 1, 0) + default: + response.BadRequestError(c, http.StatusBadRequest, "Invalid period. Use: today, 7d, 30d, this_month") + return nil + } + + 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} + } + + var limitCount int32 = 10 + if raw := c.Query("component_quantities"); raw != "" { + val, err := strconv.ParseInt(raw, 10, 32) + if err != nil { + response.BadRequestError(c, http.StatusBadRequest, "Invalid component_quantities") + return nil + } + limitCount = int32(val) + } + + data, err := repositories.GetTopExportedComponents(c.Request.Context(), global.Queries, startDate, endDate, warehouseID, limitCount) + if err != nil { + log.Err(err).Msg("Error when Get Top Exported Components") + response.InternalServerError(c, http.StatusInternalServerError, "Failed to get top exported components") + return nil + } + response.Ok(c, "Success", data) + return nil +} diff --git a/pkg/helper/jwt.go b/pkg/helper/jwt.go index b310e20..5dadae3 100644 --- a/pkg/helper/jwt.go +++ b/pkg/helper/jwt.go @@ -22,21 +22,21 @@ func GenerateToken(userID string) (string, error) { } func ParseToken(tokenString string) (jwt.MapClaims, error) { - log.Debug().Str("token", tokenString).Msg("Parsing JWT token") + // log.Debug().Str("token", tokenString).Msg("Parsing JWT token") token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(global.Cfg.JWT.SecretKey), nil // <-- lấy từ config }) - log.Debug().Interface("token", token).Msg("Parsed JWT token object") + // log.Debug().Interface("token", token).Msg("Parsed JWT token object") if err != nil { return nil, err } - log.Debug().Interface("claims", token.Claims).Msg("Parsed JWT claims") + // log.Debug().Interface("claims", token.Claims).Msg("Parsed JWT claims") if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { - log.Debug().Interface("claims", claims).Msg("Valid JWT claims extracted") + // log.Debug().Interface("claims", claims).Msg("Valid JWT claims extracted") return claims, nil } log.Error().Msg("Invalid JWT token: claims not valid or token not valid") diff --git a/sqlc_gen/dashboard.sql.go b/sqlc_gen/dashboard.sql.go index d5a06e2..cb7c701 100644 --- a/sqlc_gen/dashboard.sql.go +++ b/sqlc_gen/dashboard.sql.go @@ -232,6 +232,71 @@ func (q *Queries) GetTodayInvoiceCounts(ctx context.Context) ([]GetTodayInvoiceC return items, nil } +const getTopExportedComponents = `-- name: GetTopExportedComponents :many +SELECT c.id, c.name, c.unit, ct.name AS component_type_name, + COALESCE(SUM(st.quantity), 0)::bigint AS total_exported +FROM stock_transactions st +JOIN components c ON c.id = st.component_id +LEFT JOIN component_types ct ON c.component_type_id = ct.id +JOIN containers con ON st.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 st.transaction_type = 'export' + AND st.created_at >= $1::timestamptz + AND st.created_at < $2::timestamptz + AND ($3::bigint IS NULL OR r.warehouse_id = $3::bigint) +GROUP BY c.id, c.name, c.unit, ct.name +ORDER BY total_exported DESC +LIMIT $4::int +` + +type GetTopExportedComponentsParams struct { + StartDate time.Time `db:"start_date" json:"startDate"` + EndDate time.Time `db:"end_date" json:"endDate"` + WarehouseID pgtype.Int8 `db:"warehouse_id" json:"warehouseId"` + LimitCount int32 `db:"limit_count" json:"limitCount"` +} + +type GetTopExportedComponentsRow struct { + ID int64 `db:"id" json:"id"` + Name string `db:"name" json:"name"` + Unit string `db:"unit" json:"unit"` + ComponentTypeName pgtype.Text `db:"component_type_name" json:"componentTypeName"` + TotalExported int64 `db:"total_exported" json:"totalExported"` +} + +func (q *Queries) GetTopExportedComponents(ctx context.Context, arg GetTopExportedComponentsParams) ([]GetTopExportedComponentsRow, error) { + rows, err := q.db.Query(ctx, getTopExportedComponents, + arg.StartDate, + arg.EndDate, + arg.WarehouseID, + arg.LimitCount, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetTopExportedComponentsRow + for rows.Next() { + var i GetTopExportedComponentsRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Unit, + &i.ComponentTypeName, + &i.TotalExported, + ); 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 diff --git a/sqlc_gen/querier.go b/sqlc_gen/querier.go index 599420f..bdbb017 100644 --- a/sqlc_gen/querier.go +++ b/sqlc_gen/querier.go @@ -70,6 +70,7 @@ type Querier interface { GetShelveByID(ctx context.Context, id int64) (Shelf, error) GetStockAlerts(ctx context.Context) ([]GetStockAlertsRow, error) GetTodayInvoiceCounts(ctx context.Context) ([]GetTodayInvoiceCountsRow, error) + GetTopExportedComponents(ctx context.Context, arg GetTopExportedComponentsParams) ([]GetTopExportedComponentsRow, error) GetTotalComponentStats(ctx context.Context, warehouseID pgtype.Int8) (GetTotalComponentStatsRow, error) GetTransactionChartData(ctx context.Context, arg GetTransactionChartDataParams) ([]GetTransactionChartDataRow, error) GetUserByEmail(ctx context.Context, email string) (User, error)