Compare commits

..

1 Commits

Author SHA1 Message Date
aguhh18 28c6aaefac feat(BE): refactor search queries to be case insensitive across all modules 2026-01-11 21:09:15 +07:00
110 changed files with 1308 additions and 5402 deletions
Vendored
BIN
View File
Binary file not shown.
-3
View File
@@ -13,9 +13,7 @@ bin/
Makefile
docker-compose.local.yml
docker-compose.yaml
Dockerfile
Dockerfile.local
.gitlab-ci.yml
# Go build cache
.gocache/
vendor
@@ -29,4 +27,3 @@ coverage/
.vscode/
.idea/
*.swp
.DS_Store
+11 -29
View File
@@ -1,38 +1,20 @@
# =========================
# Builder stage
# =========================
FROM golang:1.23-alpine AS builder
FROM golang:1.23-alpine
RUN apk add --no-cache git ca-certificates tzdata
WORKDIR /app
# Install dependensi dasar
RUN apk add --no-cache git curl bash build-base
# Install Air (pakai repo baru air-verse)
RUN go install github.com/air-verse/air@v1.52.3
WORKDIR /lti-api
# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build API binary
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-s -w" -o lti-api ./cmd/api
# Build SEED binary (pastikan cmd/seed ada)
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-s -w" -o lti-seed ./cmd/seed
# =========================
# Runtime stage
# =========================
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata curl bash postgresql-client \
&& adduser -D -H -u 10001 appuser
WORKDIR /app
COPY --from=builder /app/lti-api /app/lti-api
COPY --from=builder /app/lti-seed /app/lti-seed
USER appuser
EXPOSE 8081
CMD ["/app/lti-api"]
CMD ["air", "-c", ".air.toml"]
+3 -3
View File
@@ -106,8 +106,8 @@ internal/
## ✨ Author
IT Development PT Mitra Berlian Unggas Groups
IT Development PT Mitra Berlian Unggas Group
## 📃 Licensee
## 📃 License
> This project is private. All rights reserved.
This project is private. All rights reserved.
+77
View File
@@ -0,0 +1,77 @@
services:
postgresdb:
image: postgres:alpine
restart: always
ports:
- "${DB_PORT_HOST:-5542}:5432"
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
POSTGRES_DB: ${DB_NAME:-db_lti_erp}
volumes:
- dbdata:/var/lib/postgresql/data
- ./internal/database/init:/docker-entrypoint-initdb.d
networks: [go-network]
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}",
]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
ports:
- "${REDIS_PORT_HOST:-6381}:6379"
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
interval: 5s
timeout: 3s
retries: 10
networks: [go-network]
app:
build:
context: .
dockerfile: Dockerfile.local
image: cosmtrek/air:v1.52.3
working_dir: /lti-api
volumes:
- .:/lti-api
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
command: air -c .air.toml
env_file:
- .env
environment:
DB_HOST: postgresdb
DB_PORT: 5432
DB_USER: ${DB_USER:-postgres}
DB_PASSWORD: ${DB_PASSWORD:-postgres}
DB_NAME: ${DB_NAME:-db_lti_erp}
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
ports:
- "${APP_PORT:-8081}:8081"
depends_on:
postgresdb:
condition: service_healthy
networks: [go-network]
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
interval: 10s
timeout: 3s
retries: 10
start_period: 10s
volumes:
dbdata:
go-mod-cache:
go-build-cache:
networks:
go-network:
name: lti-api_go-network
driver: bridge
+98
View File
@@ -0,0 +1,98 @@
services:
dev-api-lti:
build:
context: .
dockerfile: Dockerfile
container_name: dev-api-lti
working_dir: /lti-api
command: ["/bin/sh", "scripts/entrypoint.sh"]
ports:
- "8081:8081"
env_file:
- .env
environment:
# override agar koneksi ke container internal
DB_HOST: dev-postgres-lti
DB_PORT: 5432
REDIS_URL: redis://dev-redis-lti:6379/0
volumes:
- .:/lti-api
- ./.air.toml:/lti-api/.air.toml:ro
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
depends_on:
- dev-postgres-lti
- dev-redis-lti
networks:
- lti-network
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
interval: 10s
timeout: 3s
retries: 10
start_period: 10s
deploy:
resources:
limits:
cpus: "2.0"
memory: 2G
reservations:
cpus: "1.0"
memory: 512M
dev-postgres-lti:
image: postgres:15-alpine
container_name: dev-postgres-lti
restart: always
env_file:
- credential/.env.db
ports:
- "5433:5432"
volumes:
- dev-postgres-lti-data:/var/lib/postgresql/data
- ./credential:/docker-entrypoint-initdb.d:ro
networks:
- lti-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
deploy:
resources:
limits:
cpus: "1.0"
memory: 2G
reservations:
cpus: "0.5"
memory: 512M
dev-redis-lti:
image: redis:7-alpine
container_name: dev-redis-lti
restart: always
ports:
- "6380:6379"
networks:
- lti-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 10
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
reservations:
cpus: "0.2"
memory: 256M
networks:
lti-network:
driver: bridge
volumes:
dev-postgres-lti-data:
+1 -1
View File
@@ -16,7 +16,6 @@ require (
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/jackc/pgconn v1.14.1
github.com/jackc/pgx/v5 v5.5.5
github.com/redis/go-redis/v9 v9.14.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.19.0
@@ -61,6 +60,7 @@ require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
+4
View File
@@ -262,10 +262,14 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -1,79 +0,0 @@
-- Rollback: Revert FIFO fields back to laying_transfers from detail tables
-- ============================================================================
-- PART 1: Remove FIFO columns from detail tables
-- ============================================================================
-- Add back old qty column first
ALTER TABLE laying_transfer_sources
ADD COLUMN IF NOT EXISTS qty NUMERIC(15, 3) NOT NULL DEFAULT 0;
ALTER TABLE laying_transfer_targets
ADD COLUMN IF NOT EXISTS qty NUMERIC(15, 3) NOT NULL DEFAULT 0;
-- Now drop FIFO columns
ALTER TABLE laying_transfer_sources
DROP COLUMN IF EXISTS usage_qty,
DROP COLUMN IF EXISTS pending_usage_qty;
ALTER TABLE laying_transfer_targets
DROP COLUMN IF EXISTS total_qty,
DROP COLUMN IF EXISTS total_used;
-- ============================================================================
-- PART 2: Add back FIFO columns to laying_transfers table
-- ============================================================================
-- Add columns back for USABLE role (source warehouse)
ALTER TABLE laying_transfers
ADD COLUMN product_warehouse_id BIGINT,
ADD COLUMN pending_usage_qty NUMERIC(15, 3),
ADD COLUMN usage_qty NUMERIC(15, 3);
-- Add columns back for STOCKABLE role (destination warehouse)
ALTER TABLE laying_transfers
ADD COLUMN dest_product_warehouse_id BIGINT,
ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL,
ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0 NOT NULL;
-- ============================================================================
-- PART 3: Recreate foreign key constraints
-- ============================================================================
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
-- Add source product warehouse FK
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_transfers_product_warehouse_id
FOREIGN KEY (product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE SET NULL;
-- Add destination product warehouse FK
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_transfers_dest_product_warehouse_id
FOREIGN KEY (dest_product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE SET NULL;
END IF;
END $$;
-- ============================================================================
-- PART 4: Recreate indexes for performance
-- ============================================================================
CREATE INDEX idx_laying_transfers_product_warehouse_id
ON laying_transfers(product_warehouse_id);
CREATE INDEX idx_laying_transfers_dest_product_warehouse_id
ON laying_transfers(dest_product_warehouse_id);
-- ============================================================================
-- PART 5: Recreate comments for documentation
-- ============================================================================
COMMENT ON COLUMN laying_transfers.product_warehouse_id IS 'Product warehouse at source (Growing flock) - for USABLE role';
COMMENT ON COLUMN laying_transfers.dest_product_warehouse_id IS 'Product warehouse at destination (Laying flock) - for STOCKABLE role';
COMMENT ON COLUMN laying_transfers.total_qty IS 'Total lot quantity introduced to destination warehouse - for STOCKABLE role';
COMMENT ON COLUMN laying_transfers.total_used IS 'Quantity already consumed from this lot at destination - for FIFO STOCKABLE role';
@@ -1,73 +0,0 @@
-- Move FIFO fields from laying_transfers to detail tables (sources & targets)
-- This enables proper FIFO integration for transfer laying with multiple sources and targets
-- ============================================================================
-- PART 1: Remove FIFO-related columns from laying_transfers table
-- ============================================================================
-- Drop foreign key constraints first
DO $$
BEGIN
-- Drop source product warehouse FK
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'fk_laying_transfers_product_warehouse_id'
) THEN
ALTER TABLE laying_transfers
DROP CONSTRAINT fk_laying_transfers_product_warehouse_id;
END IF;
-- Drop destination product warehouse FK
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'fk_laying_transfers_dest_product_warehouse_id'
) THEN
ALTER TABLE laying_transfers
DROP CONSTRAINT fk_laying_transfers_dest_product_warehouse_id;
END IF;
END $$;
-- Drop indexes
DROP INDEX IF EXISTS idx_laying_transfers_product_warehouse_id;
DROP INDEX IF EXISTS idx_laying_transfers_dest_product_warehouse_id;
-- Remove columns from laying_transfers
ALTER TABLE laying_transfers
DROP COLUMN IF EXISTS product_warehouse_id,
DROP COLUMN IF EXISTS dest_product_warehouse_id,
DROP COLUMN IF EXISTS pending_usage_qty,
DROP COLUMN IF EXISTS usage_qty,
DROP COLUMN IF EXISTS total_qty,
DROP COLUMN IF EXISTS total_used;
-- ============================================================================
-- PART 2: Add FIFO columns to laying_transfer_sources (USABLE role)
-- ============================================================================
ALTER TABLE laying_transfer_sources
ADD COLUMN usage_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL,
ADD COLUMN pending_usage_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL;
-- Add comments for documentation
COMMENT ON COLUMN laying_transfer_sources.usage_qty IS 'Quantity consumed from this source - for FIFO USABLE role';
COMMENT ON COLUMN laying_transfer_sources.pending_usage_qty IS 'Quantity pending to consume from this source - for FIFO USABLE role';
-- Drop old qty column as it's replaced by usage_qty
ALTER TABLE laying_transfer_sources
DROP COLUMN IF EXISTS qty;
-- ============================================================================
-- PART 3: Add FIFO columns to laying_transfer_targets (STOCKABLE role)
-- ============================================================================
ALTER TABLE laying_transfer_targets
ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL,
ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0 NOT NULL;
-- Add comments for documentation
COMMENT ON COLUMN laying_transfer_targets.total_qty IS 'Total lot quantity introduced to this target warehouse - for FIFO STOCKABLE role';
COMMENT ON COLUMN laying_transfer_targets.total_used IS 'Quantity already consumed from this lot at target warehouse - for FIFO STOCKABLE role';
-- Drop old qty column as it's replaced by total_qty
ALTER TABLE laying_transfer_targets
DROP COLUMN IF EXISTS qty;
@@ -1,6 +0,0 @@
-- Rollback: remove price from supplier relations
ALTER TABLE product_suppliers
DROP COLUMN IF EXISTS price;
ALTER TABLE nonstock_suppliers
DROP COLUMN IF EXISTS price;
@@ -1,6 +0,0 @@
-- Migration: add price to supplier relations
ALTER TABLE product_suppliers
ADD COLUMN IF NOT EXISTS price NUMERIC(15, 3) NOT NULL DEFAULT 0;
ALTER TABLE nonstock_suppliers
ADD COLUMN IF NOT EXISTS price NUMERIC(15, 3) NOT NULL DEFAULT 0;
-18
View File
@@ -1,18 +0,0 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Dashboard struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+13
View File
@@ -12,6 +12,17 @@ type LayingTransfer struct {
FromProjectFlockId uint `gorm:"not null"`
ToProjectFlockId uint `gorm:"not null"`
TransferDate time.Time `gorm:"type:date;not null"`
PendingUsageQty *float64 `gorm:"type:numeric(15,3)"`
UsageQty *float64 `gorm:"type:numeric(15,3)"`
ProductWarehouseId *uint `gorm:"type:bigint"` // Source PW (PULLET)
DestProductWarehouseID *uint `gorm:"column:dest_product_warehouse_id;type:bigint"` // Destination PW (LAYER)
TotalQty float64 `gorm:"column:total_qty;type:numeric(15,3);default:0"` // Total lot introduced to destination
TotalUsed float64 `gorm:"column:total_used;type:numeric(15,3);default:0"` // Already consumed from this lot
Notes string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
@@ -20,6 +31,8 @@ type LayingTransfer struct {
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` // Source PW
DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID;references:Id"` // Destination PW
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
+1 -2
View File
@@ -11,8 +11,7 @@ type LayingTransferSource struct {
LayingTransferId uint `gorm:"index;not null"`
SourceProjectFlockKandangId uint `gorm:"not null"`
ProductWarehouseId *uint `gorm:""`
UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
Qty float64 `gorm:"type:numeric(15,3);not null"`
Note string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
+1 -2
View File
@@ -10,8 +10,7 @@ type LayingTransferTarget struct {
Id uint `gorm:"primaryKey"`
LayingTransferId uint `gorm:"index;not null"`
TargetProjectFlockKandangId uint `gorm:"not null"`
TotalQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO STOCKABLE field
TotalUsed float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO STOCKABLE field
Qty float64 `gorm:"type:numeric(15,3);not null"`
ProductWarehouseId *uint `gorm:""`
Note string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"`
-1
View File
@@ -5,7 +5,6 @@ import "time"
type NonstockSupplier struct {
NonstockId uint `gorm:"not null"`
SupplierId uint `gorm:"not null"`
Price float64 `gorm:"type:numeric(15,3);not null;default:0"`
CreatedAt time.Time `gorm:"autoCreateTime"`
Nonstock Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
+6 -7
View File
@@ -7,13 +7,12 @@ import (
)
type Phases struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
IsActive bool `gorm:"not null;default:true"`
Category string `gorm:"type:category_code;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
ActivityCount int `gorm:"-" json:"-"`
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
IsActive bool `gorm:"not null;default:true"`
Category string `gorm:"type:category_code;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Activities []PhaseActivity `gorm:"foreignKey:PhaseId;references:Id"`
}
-1
View File
@@ -5,7 +5,6 @@ import "time"
type ProductSupplier struct {
ProductId uint `gorm:"not null"`
SupplierId uint `gorm:"not null"`
Price float64 `gorm:"type:numeric(15,3);not null;default:0"`
CreatedAt time.Time `gorm:"autoCreateTime"`
Product Product `gorm:"foreignKey:ProductId;references:Id"`
+32 -38
View File
@@ -1,8 +1,5 @@
package middleware
const(
P_DashboardGetAll = "lti.dashboard.list"
)
// project-flock
const (
P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing"
@@ -22,19 +19,18 @@ const (
)
const (
P_ExpenseGetAll = "lti.expense.list"
P_ExpenseCreateOne = "lti.expense.create"
P_ExpenseUpdateOne = "lti.expense.update"
P_ExpenseGetOne = "lti.expense.detail"
P_ExpenseDeleteOne = "lti.expense.delete"
P_ExpenseApprovalHeadArea = "lti.expense.approve.head_area"
P_ExpenseApprovalFinance = "lti.expense.approve.finance"
P_ExpenseApprovalUnitVicePresident = "lti.expense.approve.unit_vice_president"
P_ExpenseCreateRealizations = "lti.expense.create.realization"
P_ExpenseUpdateRealizations = "lti.expense.update.realization"
P_ExpenseCompleteExpense = "lti.expense.complete.expense"
P_ExpenseDocument = "lti.expense.document"
P_ExpenseDocumentRealizations = "lti.expense.document.realization"
P_ExpenseGetAll = "lti.expense.list"
P_ExpenseCreateOne = "lti.expense.create"
P_ExpenseUpdateOne = "lti.expense.update"
P_ExpenseGetOne = "lti.expense.detail"
P_ExpenseDeleteOne = "lti.expense.delete"
P_ExpenseApprovalManager = "lti.expense.approve.manager"
P_ExpenseApprovalFinance = "lti.expense.approve.finance"
P_ExpenseCreateRealizations = "lti.expense.create.realization"
P_ExpenseUpdateRealizations = "lti.expense.update.realization"
P_ExpenseCompleteExpense = "lti.expense.complete.expense"
P_ExpenseDocument = "lti.expense.document"
P_ExpenseDocumentRealizations = "lti.expense.document.realization"
)
const (
P_AdjustmentGetAll = "lti.inventory.list"
@@ -48,9 +44,7 @@ const (
P_ReportExpenseGetAll = "lti.repport.expense.list"
P_ReportDeliveryGetAll = "lti.repport.delivery.list"
P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list"
P_ReportDebtSupplierGetAll = "lti.repport.debtsupplier.list"
P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list"
P_ReportProductionResultGetAll = "lti.repport.production_result.list"
)
const (
@@ -140,18 +134,18 @@ const (
P_NonstocksUpdateOne = "lti.master.nonstocks.update"
P_NonstocksDeleteOne = "lti.master.nonstocks.delete"
P_ProductCategoriesGetAll = "lti.master.product_categories.list"
P_ProductCategoriesGetOne = "lti.master.product_categories.detail"
P_ProductCategoriesCreateOne = "lti.master.product_categories.create"
P_ProductCategoriesUpdateOne = "lti.master.product_categories.update"
P_ProductCategoriesDeleteOne = "lti.master.product_categories.delete"
P_ProductCategoriesGetAll = "lti.master.Product_categories.list"
P_ProductCategoriesGetOne = "lti.master.Product_categories.detail"
P_ProductCategoriesCreateOne = "lti.master.Product_categories.create"
P_ProductCategoriesUpdateOne = "lti.master.Product_categories.update"
P_ProductCategoriesDeleteOne = "lti.master.Product_categories.delete"
P_ProductsGetAll = "lti.master.Products.list"
P_ProductsGetOne = "lti.master.Products.detail"
P_ProductsCreateOne = "lti.master.Products.create"
P_ProductsUpdateOne = "lti.master.Products.update"
P_ProductsDeleteOne = "lti.master.Products.delete"
P_ProductsGetAll = "lti.master.products.list"
P_ProductsGetOne = "lti.master.products.detail"
P_ProductsCreateOne = "lti.master.products.create"
P_ProductsUpdateOne = "lti.master.products.update"
P_ProductsDeleteOne = "lti.master.products.delete"
P_SuppliersGetAll = "lti.master.suppliers.list"
P_SuppliersGetOne = "lti.master.suppliers.detail"
P_SuppliersCreateOne = "lti.master.suppliers.create"
@@ -213,15 +207,15 @@ const (
)
const (
P_PurchaseGetAll = "lti.purchase.list"
P_PurchaseGetOne = "lti.purchase.detail"
P_PurchaseCreateOne = "lti.purchase.create"
P_PurchaseUpdateOne = "lti.purchase.update"
P_PurchaseDeleteOne = "lti.purchase.delete"
P_PurchaseItemDeleteOne = "lti.purchase.delete.item"
P_PurchaseReceive = "lti.purchase.receive"
P_PurchaseApprovalStaff = "lti.purchase.approve.staff"
P_PurchaseApprovalManager = "lti.purchase.approve.manager"
P_PurchaseGetAll = "lti.Purchase.list"
P_PurchaseGetOne = "lti.Purchase.detail"
P_PurchaseCreateOne = "lti.Purchase.create"
P_PurchaseUpdateOne = "lti.Purchase.update"
P_PurchaseDeleteOne = "lti.Purchase.delete"
P_PurchaseItemDeleteOne = "lti.Purchase.delete.item"
P_PurchaseReceive = "lti.Purchase.receive"
P_PurchaseApprovalStaff = "lti.Purchase.approve.staff"
P_PurchaseApprovalManager = "lti.Purchase.approve.manager"
)
const (
@@ -78,36 +78,6 @@ func (u *ClosingController) GetOne(c *fiber.Ctx) error {
})
}
func (u *ClosingController) GetOverheadByProjectFlockKandang(c *fiber.Ctx) error {
projectParam := c.Params("project_flock_id")
kandangParam := c.Params("project_flock_kandang_id")
projectFlockID, err := strconv.Atoi(projectParam)
if err != nil || projectFlockID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
pfkID, err := strconv.Atoi(kandangParam)
if err != nil || pfkID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
}
kandangID := uint(pfkID)
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID), &kandangID)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get overhead by project flock kandang successfully",
Data: result,
})
}
func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
param := c.Params("projectFlockId")
@@ -116,17 +86,7 @@ func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
}
var kandangID *uint
if raw := c.Query("kandang_id"); raw != "" {
kandangInt, convErr := strconv.Atoi(raw)
if convErr != nil || kandangInt <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
kandangUint := uint(kandangInt)
kandangID = &kandangUint
}
result, err := u.ClosingService.GetClosingSummary(c, uint(id), kandangID)
result, err := u.ClosingService.GetClosingSummary(c, uint(id))
if err != nil {
return err
}
@@ -148,7 +108,12 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
}
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID), nil)
projectFlock, err := u.ClosingService.GetProjectFlockByID(c, uint(projectFlockID))
if err != nil {
return err
}
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID))
if err != nil {
return err
}
@@ -158,60 +123,19 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
Code: fiber.StatusOK,
Status: "success",
Message: "Get closing penjualan successfully",
Data: dto.ToPenjualanRealisasiResponseDTO(uint(projectFlockID), result),
})
}
func (u *ClosingController) GetPenjualanByProjectFlockKandang(c *fiber.Ctx) error {
projectParam := c.Params("project_flock_id")
kandangParam := c.Params("project_flock_kandang_id")
projectFlockID, err := strconv.Atoi(projectParam)
if err != nil || projectFlockID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
pfkID, err := strconv.Atoi(kandangParam)
if err != nil || pfkID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
}
kandangID := uint(pfkID)
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID), &kandangID)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get closing penjualan by project flock kandang successfully",
Data: dto.ToPenjualanRealisasiResponseDTO(uint(projectFlockID), result),
Data: dto.ToPenjualanRealisasiResponseDTO(projectFlock.Category, uint(projectFlockID), result),
})
}
func (u *ClosingController) GetOverhead(c *fiber.Ctx) error {
projectParam := c.Params("project_flock_id")
kandangParam := c.Params("project_flock_kandang_id")
param := c.Params("project_flock_id")
projectFlockID, err := strconv.Atoi(projectParam)
if err != nil || projectFlockID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
projectFlockID, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
}
var projectFlockKandangID *uint
if kandangParam != "" {
pfkID, err := strconv.Atoi(kandangParam)
if err != nil || pfkID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
}
kandangID := uint(pfkID)
projectFlockKandangID = &kandangID
}
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID), projectFlockKandangID)
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID))
if err != nil {
return err
}
@@ -238,14 +162,6 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
}
if raw := c.Query("kandang_id"); raw != "" {
kandangInt, convErr := strconv.Atoi(raw)
if convErr != nil || kandangInt <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
kandangUint := uint(kandangInt)
query.KandangID = &kandangUint
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
@@ -422,18 +338,7 @@ func (u *ClosingController) GetClosingDataProduksi(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
}
var kandangID *uint
if raw := c.Query("kandang_id"); raw != "" {
kandangInt, convErr := strconv.Atoi(raw)
if convErr != nil || kandangInt <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
kandangUint := uint(kandangInt)
kandangID = &kandangUint
}
result, err := u.ClosingService.GetClosingDataProduksi(c, uint(id), kandangID)
result, err := u.ClosingService.GetClosingDataProduksi(c, uint(id))
if err != nil {
return err
}
+16 -42
View File
@@ -59,65 +59,39 @@ type ClosingSummaryDTO struct {
StatusClosing string `json:"closing_status"`
}
type ClosingSummaryKandangDTO struct {
FlockID uint `json:"flock_id"`
Period int `json:"period"`
LocationName string `json:"location_name"`
Population int `json:"population"`
PopulationFormatted string `json:"population_formatted"`
ProjectType string `json:"project_type"`
ClosingDate string `json:"closing_date"`
KandangName string `json:"kandang_name"`
ChickInDate string `json:"chick_in_date"`
PicName string `json:"pic_name"`
ApprovalDate string `json:"approval_date"`
ProjectStatus string `json:"project_status"`
}
type ClosingPurchaseDTO struct {
InitialPopulation int `json:"initial_population"`
ClaimCulling int `json:"claim_culling"`
FinalPopulation int `json:"final_population"`
FeedIn float64 `json:"feed_in"`
FeedUsed float64 `json:"feed_used"`
// FeedUsedPerHead float64 `json:"feed_used_per_head"`
FeedUsedPerHead float64 `json:"feed_used_per_head"`
}
type ClosingSalesDTO struct {
SalesPopulation int `json:"sales_population"`
SalesWeight float64 `json:"sales_weight"`
AverageWeight float64 `json:"avg_weight"`
AverageSellingPrice float64 `json:"avg_selling_price"`
AverageWeight float64 `json:"average_weight"`
AverageSellingPrice float64 `json:"chicken_average_selling_price"`
}
type ClosingEggSalesDTO struct {
EggPieces int `json:"egg_pieces"`
EggMassKg float64 `json:"egg_mass"`
AverageEggWeightKg float64 `json:"avg_egg_weight"`
AverageSellingPrice float64 `json:"avg_selling_price"`
EggMassKg float64 `json:"egg_mass_kg"`
AverageEggWeightKg float64 `json:"average_egg_weight_kg"`
AverageSellingPrice float64 `json:"egg_average_selling_price"`
}
type ClosingPerformanceDTO struct {
Depletion float64 `json:"depletion"`
Age float64 `json:"age_day"`
MortalityStd float64 `json:"mor_std"`
MortalityAct float64 `json:"mor_act"`
DeffMortality float64 `json:"mor_diff"`
FcrStd float64 `json:"fcr_std"`
FcrAct float64 `json:"fcr_act"`
DeffFcr float64 `json:"fcr_diff"`
AwgAct float64 `json:"awg_act"`
AwgStd float64 `json:"awg_std"`
FeedIntake float64 `json:"feed_intake"`
FeedIntakeStd float64 `json:"feed_intake_std"`
HenDayAct *float64 `json:"hen_day_act,omitempty"`
HendayStd *float64 `json:"hen_day_std,omitempty"`
EggMass *float64 `json:"egg_mass,omitempty"`
EggMassStd *float64 `json:"egg_mass_std,omitempty"`
EggWeight *float64 `json:"egg_weight,omitempty"`
EggWeightStd *float64 `json:"egg_weight_std,omitempty"`
HenHouseAct *float64 `json:"hen_housed_act,omitempty"`
HenHouseStd *float64 `json:"hen_housed_std,omitempty"`
Depletion float64 `json:"depletion"`
Age float64 `json:"age_day"`
MortalityStd float64 `json:"mortality_std"`
MortalityAct float64 `json:"mortality_act"`
DeffMortality float64 `json:"deff_mortality"`
FcrStd float64 `json:"fcr_std"`
FcrAct float64 `json:"fcr_act"`
DeffFcr float64 `json:"deff_fcr"`
Awg float64 `json:"awg"`
}
type ClosingSalesGroupDTO struct {
@@ -190,7 +164,7 @@ func sumPopulation(history []entity.ProjectFlockKandang) float64 {
var total float64
for _, h := range history {
for _, chickin := range h.Chickins {
total += chickin.UsageQty
total += chickin.UsageQty + chickin.PendingUsageQty
}
}
return total
@@ -79,11 +79,10 @@ type HppGroup struct {
}
type SummaryHpp struct {
Label string `json:"label"`
Budgeting FinancialMetrics `json:"budgeting"`
Realization FinancialMetrics `json:"realization"`
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
Label string `json:"label"`
Comparison `json:"-"`
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
}
type HppPurchasesSection struct {
@@ -247,9 +246,11 @@ func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets [
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced)
summary := SummaryHpp{
Label: label,
Budgeting: ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
Realization: ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
Label: label,
Comparison: ToComparison(
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
),
}
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) && ctx.TotalEggWeightKg > 0 {
@@ -87,7 +87,7 @@ func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO {
return result
}
func ToPenjualanRealisasiResponseDTO(projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
func ToPenjualanRealisasiResponseDTO(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
return PenjualanRealisasiResponseDTO{
@@ -1,8 +1,6 @@
package dto
import (
"encoding/json"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
)
@@ -71,7 +69,7 @@ func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseReal
return dto
}
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64, isPerKandang bool, totalKandangCount int, projectFlockKandangCountMap map[uint]int) OverheadListDTO {
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64) OverheadListDTO {
overheadsByNonstockID := make(map[uint]*OverheadDTO)
latestDateByNonstockID := make(map[uint]string)
@@ -84,20 +82,9 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
itemName, itemUOM := getItemInfo(budgets[i].Nonstock)
overheadsByNonstockID[nonstockID].ItemName = itemName
overheadsByNonstockID[nonstockID].UOMName = itemUOM
budgetQty := budgets[i].Qty
budgetPrice := budgets[i].Price
budgetTotal := calculateTotal(budgets[i].Qty, budgets[i].Price)
// Budget division: per kandang view only
if isPerKandang && totalKandangCount > 0 {
budgetQty = budgetQty / float64(totalKandangCount)
budgetTotal = budgetTotal / float64(totalKandangCount)
}
overheadsByNonstockID[nonstockID].BudgetQuantity = budgetQty
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgetPrice
overheadsByNonstockID[nonstockID].BudgetTotalAmount = budgetTotal
overheadsByNonstockID[nonstockID].BudgetQuantity = budgets[i].Qty
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgets[i].Price
overheadsByNonstockID[nonstockID].BudgetTotalAmount = calculateTotal(budgets[i].Qty, budgets[i].Price)
}
for i := range realizations {
@@ -110,40 +97,8 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
}
qty := realizations[i].Qty
totalAmount := calculateTotal(realizations[i].Qty, realizations[i].Price)
// Farm-level expense division
if realizations[i].ExpenseNonstock.Expense != nil &&
realizations[i].ExpenseNonstock.Expense.ProjectFlockId != nil {
projectFlockIDs := parseProjectFlockIDsFromJSON(*realizations[i].ExpenseNonstock.Expense.ProjectFlockId)
if len(projectFlockIDs) > 0 {
totalKandangInAllProjects := 0
for _, pfID := range projectFlockIDs {
if count, exists := projectFlockKandangCountMap[pfID]; exists {
totalKandangInAllProjects += count
}
}
if totalKandangInAllProjects > 0 {
if isPerKandang {
qty = qty / float64(totalKandangInAllProjects)
totalAmount = totalAmount / float64(totalKandangInAllProjects)
} else {
// Overhead ALL: divide by total kandang then multiply by this project's kandang count
perKandangAmount := totalAmount / float64(totalKandangInAllProjects)
perKandangQty := qty / float64(totalKandangInAllProjects)
qty = perKandangQty * float64(totalKandangCount)
totalAmount = perKandangAmount * float64(totalKandangCount)
}
}
}
}
overheadsByNonstockID[nonstockID].ActualQuantity += qty
overheadsByNonstockID[nonstockID].ActualTotalAmount += totalAmount
overheadsByNonstockID[nonstockID].ActualQuantity += realizations[i].Qty
overheadsByNonstockID[nonstockID].ActualTotalAmount += calculateTotal(realizations[i].Qty, realizations[i].Price)
if overheadsByNonstockID[nonstockID].ItemName == "" {
itemName, itemUOM := getItemInfo(realizations[i].ExpenseNonstock.Nonstock)
@@ -191,26 +146,7 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
}
}
func parseProjectFlockIDsFromJSON(projectFlockJSON string) []uint {
if projectFlockJSON == "" {
return []uint{}
}
var projectFlocks []uint
if err := json.Unmarshal([]byte(projectFlockJSON), &projectFlocks); err != nil {
return []uint{}
}
return projectFlocks
}
func countProjectFlocksInJSON(projectFlockJSON string) int {
projectFlocks := parseProjectFlockIDsFromJSON(projectFlockJSON)
if len(projectFlocks) == 0 {
return 1
}
return len(projectFlocks)
}
// === Helper Functions ===
func getItemInfo(nonstock *entity.Nonstock) (string, string) {
if nonstock != nil && nonstock.Id != 0 {
+1 -4
View File
@@ -11,7 +11,6 @@ import (
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
rExpenseRealization "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
@@ -34,13 +33,11 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db)
chickinRepo := rChickin.NewChickinRepository(db)
recordingRepo := rRecording.NewRecordingRepository(db)
standardGrowthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
purchaseRepo := rPurchase.NewPurchaseRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, standardGrowthDetailRepo, productionStandardDetailRepo, validate)
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate)
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
@@ -18,7 +18,6 @@ type ClosingRepository interface {
repository.BaseRepository[entity.ProjectFlock]
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error)
SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error)
SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error)
@@ -167,23 +166,6 @@ func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c
return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil
}
func (r *ClosingRepositoryImpl) SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
}
var total float64
if err := r.DB().WithContext(ctx).
Model(&entity.ProjectChickin{}).
Where("project_flock_kandang_id IN ?", projectFlockKandangIDs).
Select("COALESCE(SUM(usage_qty), 0)").
Scan(&total).Error; err != nil {
return 0, err
}
return total, nil
}
func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
-3
View File
@@ -23,10 +23,8 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
route.Get("/", m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll)
route.Get("/:project_flock_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualan)
route.Get("/:project_flock_id/:project_flock_kandang_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualanByProjectFlockKandang)
route.Get("/:projectFlockId", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSummary)
route.Get("/:project_flock_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead)
route.Get("/:project_flock_id/:project_flock_kandang_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead)
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang)
route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject)
route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak)
@@ -34,5 +32,4 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang)
route.Get("/:projectFlockId/production-data", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi)
route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan)
}
@@ -2,9 +2,7 @@ package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
"strconv"
"strings"
@@ -18,7 +16,6 @@ import (
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
productionStandardRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
@@ -35,10 +32,10 @@ import (
type ClosingService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error)
GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error)
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (any, error)
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error)
GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error)
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error)
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error)
GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error)
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error)
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error)
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
@@ -49,7 +46,6 @@ type closingService struct {
Validate *validator.Validate
Repository repository.ClosingRepository
ProjectFlockRepo projectflockRepository.ProjectflockRepository
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
MarketingRepo marketingRepository.MarketingRepository
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
ApprovalSvc commonSvc.ApprovalService
@@ -58,17 +54,14 @@ type closingService struct {
ChickinRepo chickinRepository.ProjectChickinRepository
PurchaseRepo purchaseRepository.PurchaseRepository
RecordingRepo recordingRepository.RecordingRepository
StandardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository
ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository
}
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository, productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository, validate *validator.Validate) ClosingService {
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, validate *validator.Validate) ClosingService {
return &closingService{
Log: utils.Log,
Validate: validate,
Repository: repo,
ProjectFlockRepo: projectFlockRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
MarketingRepo: marketingRepo,
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
ApprovalSvc: approvalSvc,
@@ -77,8 +70,6 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje
ChickinRepo: chickinRepo,
PurchaseRepo: purchaseRepo,
RecordingRepo: recordingRepo,
StandardGrowthDetailRepo: standardGrowthDetailRepo,
ProductionStandardDetailRepo: productionStandardDetailRepo,
}
}
@@ -103,7 +94,7 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.Cl
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withClosingRelations(db)
if params.Search != "" {
return db.Where("flock_name ILIKE ?", "%"+params.Search+"%")
return db.Where("LOWER(flock_name) LIKE LOWER(?)", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
@@ -138,9 +129,24 @@ func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.Proj
return projectFlock, nil
}
func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) {
func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) {
realisasi, err := s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlockID, projectFlockKandangID)
realisasi, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
return db.
Preload("MarketingProduct").
Preload("MarketingProduct.ProductWarehouse").
Preload("MarketingProduct.ProductWarehouse.Product").
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
Preload("MarketingProduct.ProductWarehouse.Product.Uom").
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
Preload("MarketingProduct.ProductWarehouse.Warehouse").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins").
Preload("MarketingProduct.Marketing").
Preload("MarketingProduct.Marketing.Customer").
Order("marketing_delivery_products.delivery_date DESC")
})
if err != nil {
return nil, err
}
@@ -148,16 +154,21 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectF
return []entity.MarketingDeliveryProduct{}, nil
}
return realisasi, nil
}
filtered := make([]entity.MarketingDeliveryProduct, 0, len(realisasi))
for _, item := range realisasi {
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint, kandangID *uint) (any, error) {
if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
if item.UsageQty != 0 || item.TotalWeight != 0 || item.AvgWeight != 0 ||
item.UnitPrice != 0 || item.TotalPrice != 0 {
filtered = append(filtered, item)
}
}
if kandangID != nil {
return s.getClosingSummaryByKandang(c.Context(), projectFlockID, *kandangID)
return filtered, nil
}
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) {
if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
}
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations)
@@ -180,124 +191,6 @@ func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint, kan
return &summary, nil
}
func (s closingService) getClosingSummaryByKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*dto.ClosingSummaryKandangDTO, error) {
if projectFlockID == 0 || kandangID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id or kandang id")
}
db := s.Repository.DB().WithContext(ctx)
var kandang entity.ProjectFlockKandang
if err := db.
Preload("Kandang").
Preload("Kandang.Location").
Preload("Kandang.Pic").
Where("project_flock_id = ?", projectFlockID).
Where("kandang_id = ?", kandangID).
First(&kandang).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found")
}
s.Log.Errorf("Failed get project flock kandang %d/%d: %+v", projectFlockID, kandangID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
}
var project entity.ProjectFlock
if err := db.
Select("id", "category").
First(&project, projectFlockID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
}
s.Log.Errorf("Failed get project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
var population float64
if err := db.
Table("project_flock_populations pfp").
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
Where("pc.project_flock_kandang_id = ?", kandang.Id).
Select("COALESCE(SUM(pfp.total_qty), 0)").
Scan(&population).Error; err != nil {
s.Log.Errorf("Failed to sum population for project flock kandang %d: %+v", kandang.Id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch population data")
}
var chickInDate time.Time
if err := db.
Table("project_chickins").
Where("project_flock_kandang_id = ?", kandang.Id).
Select("MIN(chick_in_date)").
Scan(&chickInDate).Error; err != nil {
s.Log.Errorf("Failed to fetch chick in date for project flock kandang %d: %+v", kandang.Id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chick in date")
}
statusProject := "Belum Selesai"
var approvalDate string
if s.ApprovalSvc != nil {
records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlockKandang.String(), &kandang.Id, 1, 1000, "")
if err != nil {
s.Log.Errorf("Failed to fetch approvals for project flock kandang %d: %+v", kandang.Id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval data")
}
var (
minStep uint16
latestActionAt time.Time
)
for _, rec := range records {
if minStep == 0 || rec.StepNumber < minStep {
minStep = rec.StepNumber
}
if latestActionAt.IsZero() || rec.ActionAt.After(latestActionAt) {
latestActionAt = rec.ActionAt
statusProject = rec.StepName
}
}
if statusProject == "" && minStep > 0 {
if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowProjectFlockKandang, approvalutils.ApprovalStep(minStep)); ok {
statusProject = label
}
}
if !latestActionAt.IsZero() {
approvalDate = latestActionAt.Format("2006-01-02")
}
}
closingDate := ""
if kandang.ClosedAt != nil {
closingDate = kandang.ClosedAt.Format("2006-01-02")
}
chickInDateStr := ""
if !chickInDate.IsZero() {
chickInDateStr = chickInDate.Format("2006-01-02")
}
populationInt := int(population)
return &dto.ClosingSummaryKandangDTO{
FlockID: projectFlockID,
Period: kandang.Period,
LocationName: kandang.Kandang.Location.Name,
Population: populationInt,
PopulationFormatted: fmt.Sprintf("%d Ekor", populationInt),
ProjectType: project.Category,
ClosingDate: closingDate,
KandangName: kandang.Kandang.Name,
ChickInDate: chickInDateStr,
PicName: kandang.Kandang.Pic.Name,
ApprovalDate: approvalDate,
ProjectStatus: statusProject,
}, nil
}
func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) {
if projectFlockID == 0 {
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
@@ -338,7 +231,7 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
var projectFlockKandangIDs []uint
if params.Type == validation.SapronakTypeOutgoing {
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID, params.KandangID)
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
if err != nil {
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
@@ -418,15 +311,12 @@ func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, proje
return ids, nil
}
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint, kandangID *uint) ([]uint, error) {
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) {
var ids []uint
query := s.Repository.DB().WithContext(ctx).
err := s.Repository.DB().WithContext(ctx).
Model(&entity.ProjectFlockKandang{}).
Where("project_flock_id = ?", projectFlockID)
if kandangID != nil {
query = query.Where("kandang_id = ?", *kandangID)
}
err := query.Order("id ASC").Pluck("id", &ids).Error
Where("project_flock_id = ?", projectFlockID).
Pluck("id", &ids).Error
if err != nil {
return nil, err
}
@@ -489,90 +379,35 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID
return statusProject, statusClosing, nil
}
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error) {
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) {
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, err
}
realizations, err := s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlockID, projectFlockKandangID)
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, err
}
projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, err
}
totalKandangCount := len(projectFlockKandangs)
// Build kandang count map for farm expense division
projectFlockKandangCountMap := make(map[uint]int)
projectFlockKandangCountMap[projectFlockID] = totalKandangCount
involvedProjectFlocks := make(map[uint]bool)
for _, realization := range realizations {
if realization.ExpenseNonstock != nil &&
realization.ExpenseNonstock.Expense != nil &&
realization.ExpenseNonstock.Expense.ProjectFlockId != nil {
var projectFlockIDs []uint
if err := json.Unmarshal([]byte(*realization.ExpenseNonstock.Expense.ProjectFlockId), &projectFlockIDs); err == nil {
for _, pfID := range projectFlockIDs {
if pfID != projectFlockID {
involvedProjectFlocks[pfID] = true
}
}
}
}
}
for pfID := range involvedProjectFlocks {
if pfKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), pfID); err == nil {
projectFlockKandangCountMap[pfID] = len(pfKandangs)
}
}
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, err
}
var totalChickinQty float64
var totalDepletion float64
for _, chickin := range chickins {
totalChickinQty += chickin.UsageQty
}
if projectFlockKandangID != nil {
for _, chickin := range chickins {
if chickin.ProjectFlockKandangId == *projectFlockKandangID {
totalChickinQty += chickin.UsageQty
}
}
var depletionResult float64
err = s.RecordingRepo.DB().WithContext(c.Context()).
Table("recording_depletions").
Select("COALESCE(SUM(recording_depletions.qty), 0)").
Joins("JOIN recordings ON recordings.id = recording_depletions.recording_id").
Where("recordings.project_flock_kandangs_id = ?", *projectFlockKandangID).
Scan(&depletionResult).Error
if err != nil {
s.Log.Warnf("GetTotalDepletionByProjectFlockKandangID error: %v", err)
} else {
totalDepletion = depletionResult
}
} else {
for _, chickin := range chickins {
totalChickinQty += chickin.UsageQty
}
totalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
}
totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
}
totalActualPopulation := totalChickinQty - totalDepletion
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation, projectFlockKandangID != nil, totalKandangCount, projectFlockKandangCountMap)
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation)
return &result, nil
}
@@ -685,22 +520,12 @@ func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, proj
return result, nil
}
func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error) {
func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error) {
if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
}
projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID, kandangID)
if err != nil {
s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs")
}
if len(projectFlockKandangIDs) == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "No project flock kandang found")
}
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withRelations)
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
}
@@ -709,29 +534,19 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
population, err := s.Repository.SumProjectChickinUsageByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
if err != nil {
s.Log.Errorf("Failed to sum population for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch population data")
var population float64
for _, history := range project.KandangHistory {
for _, chickin := range history.Chickins {
population += chickin.UsageQty
}
}
isGrowing := strings.EqualFold(project.Category, string(utils.ProjectFlockCategoryGrowing))
currentWeek, err := s.determineProductionWeek(c.Context(), projectFlockKandangIDs)
projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
if err != nil {
s.Log.Errorf("Failed to determine production week for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to determine production week")
}
targetAverages, err := s.RecordingRepo.GetAverageTargetMetricsByProjectFlockKandangID(c.Context(), projectFlockKandangIDs[0], !isGrowing)
if err != nil {
s.Log.Errorf("Failed to calculate target metrics for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch target metrics data")
}
var fcrActFromRecording *float64
if targetAverages.FcrCount > 0 {
fcrAvg := targetAverages.FcrAvg
fcrActFromRecording = &fcrAvg
s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs")
}
feedIn, feedUsed, err := s.Repository.SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
@@ -740,40 +555,6 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch feed purchase data")
}
averageFeedIntake := targetAverages.FeedIntakeAvg
feedIntakeStd := 0.0
var mortalityStdFromGrowth *float64
if project.ProductionStandardId > 0 && currentWeek > 0 && s.StandardGrowthDetailRepo != nil {
growthDetail, growthErr := s.StandardGrowthDetailRepo.GetByStandardIDAndWeek(c.Context(), project.ProductionStandardId, currentWeek)
if growthErr != nil {
if !errors.Is(growthErr, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to fetch growth detail for project flock %d: %+v", projectFlockID, growthErr)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch growth standard data")
}
} else if growthDetail != nil {
if growthDetail.FeedIntake != nil {
feedIntakeStd = *growthDetail.FeedIntake
}
if growthDetail.MaxDepletion != nil {
mortalityStdFromGrowth = growthDetail.MaxDepletion
}
}
}
var productionStandardDetail *entity.ProductionStandardDetail
if project.ProductionStandardId > 0 && currentWeek > 0 && s.ProductionStandardDetailRepo != nil {
productionStandardDetail, err = s.ProductionStandardDetailRepo.GetByStandardIDAndWeek(c.Context(), project.ProductionStandardId, currentWeek)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
productionStandardDetail = nil
} else {
s.Log.Errorf("Failed to fetch production standard detail for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch production standard detail data")
}
}
}
claimCulling, err := s.Repository.SumClaimCullingByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
if err != nil {
s.Log.Errorf("Failed to sum claim culling for project flock %d: %+v", projectFlockID, err)
@@ -796,10 +577,10 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales age data")
}
// feedUsedPerHead := 0.0
// if population > 0 {
// feedUsedPerHead = feedUsed / population
// }
feedUsedPerHead := 0.0
if population > 0 {
feedUsedPerHead = feedUsed / population
}
purchase := dto.ClosingPurchaseDTO{
InitialPopulation: int(population),
@@ -807,7 +588,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
FinalPopulation: int(finalPopulation),
FeedIn: feedIn,
FeedUsed: feedUsed,
// FeedUsedPerHead: feedUsedPerHead,
FeedUsedPerHead: feedUsedPerHead,
}
chickenFlagNames := []string{string(utils.FlagPullet)}
@@ -840,9 +621,6 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
}
chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards)
if fcrActFromRecording != nil {
chickenPerformance.FcrAct = *fcrActFromRecording
}
var eggSales *dto.ClosingEggSalesDTO
var eggPerformance *dto.ClosingPerformanceDTO
@@ -890,9 +668,6 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
}
eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards)
if fcrActFromRecording != nil {
eggPerf.FcrAct = *fcrActFromRecording
}
eggPerformance = &eggPerf
}
@@ -909,63 +684,15 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
DeffMortality: chickenPerformance.DeffMortality,
}
if eggPerformance != nil {
// performance.FcrStd = eggPerformance.FcrStd
performance.FcrStd = eggPerformance.FcrStd
performance.FcrAct = eggPerformance.FcrAct
// performance.DeffFcr = eggPerformance.DeffFcr
performance.AwgAct = eggPerformance.AwgAct
performance.DeffFcr = eggPerformance.DeffFcr
performance.Awg = eggPerformance.Awg
} else {
// performance.FcrStd = chickenPerformance.FcrStd
performance.FcrStd = chickenPerformance.FcrStd
performance.FcrAct = chickenPerformance.FcrAct
// performance.DeffFcr = chickenPerformance.DeffFcr
performance.AwgAct = chickenPerformance.AwgAct
}
performance.FeedIntake = averageFeedIntake
performance.FeedIntakeStd = feedIntakeStd
if targetAverages.CumDepletionRateCount > 0 {
performance.MortalityAct = targetAverages.CumDepletionRateAvg
performance.DeffMortality = performance.MortalityAct - performance.MortalityStd
}
if mortalityStdFromGrowth != nil {
performance.MortalityStd = *mortalityStdFromGrowth
performance.DeffMortality = performance.MortalityAct - performance.MortalityStd
}
if !isGrowing {
if targetAverages.HenDayCount > 0 {
henDayAct := targetAverages.HenDayAvg
performance.HenDayAct = &henDayAct
}
if targetAverages.HenHouseCount > 0 {
henHouseAct := targetAverages.HenHouseAvg
performance.HenHouseAct = &henHouseAct
}
if targetAverages.EggWeightCount > 0 {
eggWeight := targetAverages.EggWeightAvg
performance.EggWeight = &eggWeight
}
if targetAverages.EggMassCount > 0 {
eggMass := targetAverages.EggMassAvg
performance.EggMass = &eggMass
}
}
performance.DeffFcr = performance.FcrStd - performance.FcrAct
if productionStandardDetail != nil {
if productionStandardDetail.StandardFCR != nil {
performance.FcrStd = *productionStandardDetail.StandardFCR
}
if !isGrowing {
if productionStandardDetail.TargetHenDayProduction != nil {
performance.HendayStd = productionStandardDetail.TargetHenDayProduction
}
if productionStandardDetail.TargetHenHouseProduction != nil {
performance.HenHouseStd = productionStandardDetail.TargetHenHouseProduction
}
if productionStandardDetail.TargetEggWeight != nil {
performance.EggWeightStd = productionStandardDetail.TargetEggWeight
}
if productionStandardDetail.TargetEggMass != nil {
performance.EggMassStd = productionStandardDetail.TargetEggMass
}
}
performance.DeffFcr = chickenPerformance.DeffFcr
performance.Awg = chickenPerformance.Awg
}
result := dto.ClosingProductionReportDTO{
@@ -1011,46 +738,6 @@ func (s closingService) calculateAverageSalesAge(ctx context.Context, projectFlo
return totalAgeWeeks / totalQty, nil
}
func (s closingService) determineProductionWeek(ctx context.Context, projectFlockKandangIDs []uint) (int, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
}
firstKandangID := projectFlockKandangIDs[0]
var chickin entity.ProjectChickin
if err := s.Repository.DB().WithContext(ctx).
Where("project_flock_kandang_id = ?", firstKandangID).
Order("chick_in_date ASC").
First(&chickin).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, nil
}
return 0, err
}
recording, err := s.RecordingRepo.GetLatestByProjectFlockKandangID(ctx, firstKandangID)
if err != nil {
return 0, err
}
if recording == nil {
return 0, nil
}
if recording.RecordDatetime.Before(chickin.ChickInDate) {
return 0, nil
}
elapsed := recording.RecordDatetime.Sub(chickin.ChickInDate)
weekFloat := elapsed.Hours() / (24 * 7)
week := int(math.Ceil(weekFloat))
if week <= 0 {
week = 1
}
return week, nil
}
func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64, standards []entity.FcrStandard) dto.ClosingPerformanceDTO {
mortalityStd, fcrStd := closestFcrValues(standards, averageWeight)
@@ -1081,7 +768,7 @@ func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopul
FcrStd: fcrStd,
FcrAct: fcrAct,
DeffFcr: deffFcr,
AwgAct: awg,
Awg: awg,
}
}
@@ -20,8 +20,7 @@ const (
)
type ClosingSapronakQuery struct {
Type string `query:"type" validate:"required,oneof=incoming outgoing"`
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
KandangID *uint `query:"kandang_id" validate:"omitempty,gt=0"`
Type string `query:"type" validate:"required,oneof=incoming outgoing"`
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
}
@@ -74,7 +74,6 @@ func (u *DailyChecklistController) GetAll(c *fiber.Ctx) error {
Name: name,
Status: status,
Category: item.Category,
RejectReason: item.RejectReason,
Date: item.Date,
Kandang: kandang,
CreatedUser: nil,
@@ -151,10 +150,6 @@ func (u *DailyChecklistController) GetSummary(c *fiber.Ctx) error {
performanceMap[summary.EmployeeID] = &dto.DailyChecklistPerformanceOverviewDTO{
EmployeeID: summary.EmployeeID,
EmployeeName: summary.EmployeeName,
Kandang: dto.DailyChecklistReportEntityDTO{
Id: summary.KandangID,
Name: summary.KandangName,
},
}
}
@@ -308,22 +303,12 @@ func (u *DailyChecklistController) GetOne(c *fiber.Ctx) error {
return err
}
documentDTOs := make([]dto.DailyChecklistDocumentDTO, len(detail.DocumentURLs))
for i, doc := range detail.DocumentURLs {
documentDTOs[i] = dto.DailyChecklistDocumentDTO{
Id: doc.ID,
Name: doc.Name,
Size: doc.Size,
URL: doc.URL,
}
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get dailyChecklist successfully",
Data: dto.ToDailyChecklistDetailDTO(detail.Checklist, detail.Phases, detail.Tasks, detail.AssignedEmployees, detail.TotalActivities, detail.Progress, documentDTOs),
Data: dto.ToDailyChecklistDetailDTO(detail.Checklist, detail.Phases, detail.Tasks, detail.AssignedEmployees, detail.TotalActivities, detail.Progress),
})
}
@@ -357,12 +342,6 @@ func (u *DailyChecklistController) UpdateOne(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
form, err := c.MultipartForm()
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
}
req.Documents = form.File["documents"]
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
@@ -31,7 +31,6 @@ type DailyChecklistListDTO struct {
TotalPhase int `json:"total_phase"`
TotalActivity int `json:"total_activity"`
Progress int `json:"progress"`
RejectReason *string `json:"reject_reason"`
}
type DailyChecklistDetailDTO struct {
@@ -41,14 +40,6 @@ type DailyChecklistDetailDTO struct {
AssignedEmployees []employeeDTO.EmployeesRelationDTO `json:"assigned_employees"`
TotalActivity int `json:"total_activity"`
Progress float64 `json:"progress"`
DocumentURLs []DailyChecklistDocumentDTO `json:"document_urls"`
}
type DailyChecklistDocumentDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Size float64 `json:"size"`
URL string `json:"url"`
}
type DailyChecklistSummaryDTO struct {
@@ -64,12 +55,11 @@ type DailyChecklistSummaryDTO struct {
}
type DailyChecklistPerformanceOverviewDTO struct {
EmployeeID uint `json:"employee_id"`
EmployeeName string `json:"employee_name"`
Kandang DailyChecklistReportEntityDTO `json:"kandang"`
TotalActivity int `json:"total_activity"`
ActivityDone int `json:"activity_done"`
ActivityLeft int `json:"activity_left"`
EmployeeID uint `json:"employee_id"`
EmployeeName string `json:"employee_name"`
TotalActivity int `json:"total_activity"`
ActivityDone int `json:"activity_done"`
ActivityLeft int `json:"activity_left"`
}
type DailyChecklistReportDTO struct {
@@ -175,11 +165,10 @@ func ToDailyChecklistListDTO(e entity.DailyChecklist) DailyChecklistListDTO {
TotalPhase: 0,
TotalActivity: 0,
Progress: 0,
RejectReason: e.RejectReason,
}
}
func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity.DailyChecklistPhase, tasks []entity.DailyChecklistActivityTask, assignedEmployees []entity.Employee, totalActivities int, progress float64, documentURLs []DailyChecklistDocumentDTO) DailyChecklistDetailDTO {
func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity.DailyChecklistPhase, tasks []entity.DailyChecklistActivityTask, assignedEmployees []entity.Employee, totalActivities int, progress float64) DailyChecklistDetailDTO {
phaseDTOs := make([]DailyChecklistPhaseDTO, 0, len(phases))
for _, phase := range phases {
phaseDTOs = append(phaseDTOs, DailyChecklistPhaseDTO{
@@ -239,6 +228,5 @@ func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity.
AssignedEmployees: assignedDTOs,
TotalActivity: totalActivities,
Progress: progress,
DocumentURLs: documentURLs,
}
}
+1 -11
View File
@@ -1,15 +1,10 @@
package dailyChecklists
import (
"context"
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories"
sDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
rPhases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
@@ -24,13 +19,8 @@ func (DailyChecklistModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
dailyChecklistRepo := rDailyChecklist.NewDailyChecklistRepository(db)
phasesRepo := rPhases.NewPhasesRepository(db)
userRepo := rUser.NewUserRepository(db)
documentRepo := commonRepo.NewDocumentRepository(db)
documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo)
if err != nil {
panic(fmt.Sprintf("failed to create document service: %v", err))
}
dailyChecklistService := sDailyChecklist.NewDailyChecklistService(dailyChecklistRepo, phasesRepo, validate, documentSvc)
dailyChecklistService := sDailyChecklist.NewDailyChecklistService(dailyChecklistRepo, phasesRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
DailyChecklistRoutes(router, userService, dailyChecklistService)
+3 -3
View File
@@ -1,7 +1,7 @@
package dailyChecklists
import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/controllers"
dailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,7 +13,7 @@ func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist.
ctrl := controller.NewDailyChecklistController(s)
route := v1.Group("/daily-checklists")
route.Use(m.Auth(u))
// route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Get("/report", ctrl.GetReport)
@@ -22,7 +22,7 @@ func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist.
route.Get("/report", ctrl.GetReport)
// upsert daily checklist
// create daily checklist
route.Post("/", ctrl.CreateOne)
// get detail data daily checklist by id
@@ -9,7 +9,6 @@ import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations"
phaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
@@ -18,7 +17,6 @@ import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
@@ -41,18 +39,10 @@ type DailyChecklistService interface {
}
type dailyChecklistService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.DailyChecklistRepository
PhaseRepo phaseRepo.PhasesRepository
DocumentSvc commonSvc.DocumentService
}
type DailyChecklistDocument struct {
ID uint
Name string
Size float64
URL string
Log *logrus.Logger
Validate *validator.Validate
Repository repository.DailyChecklistRepository
PhaseRepo phaseRepo.PhasesRepository
}
type DailyChecklistDetail struct {
@@ -62,7 +52,6 @@ type DailyChecklistDetail struct {
AssignedEmployees []entity.Employee
TotalActivities int
Progress float64
DocumentURLs []DailyChecklistDocument
}
type DailyChecklistListItem struct {
@@ -71,7 +60,6 @@ type DailyChecklistListItem struct {
Date time.Time
Category string
Status *string
RejectReason *string
CreatedAt time.Time
UpdatedAt time.Time
Kandang entity.Kandang
@@ -120,13 +108,12 @@ type DailyChecklistReportCategory struct {
Baik int
}
func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate, documentSvc commonSvc.DocumentService) DailyChecklistService {
func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate) DailyChecklistService {
return &dailyChecklistService{
Log: utils.Log,
Validate: validate,
Repository: repo,
PhaseRepo: phaseRepo,
DocumentSvc: documentSvc,
Log: utils.Log,
Validate: validate,
Repository: repo,
PhaseRepo: phaseRepo,
}
}
@@ -171,7 +158,7 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
if params.Search != "" {
like := "%" + params.Search + "%"
db = db.Where("(k.name ILIKE ? OR dc.category::text ILIKE ?)", like, like)
db = db.Where("(k.name ILIKE ? OR dc.category ILIKE ?)", like, like)
}
countDB := db.Session(&gorm.Session{})
@@ -187,7 +174,6 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
Date time.Time
Category string
Status *string
RejectReason *string
CreatedAt time.Time
UpdatedAt time.Time
KandangID uint
@@ -206,7 +192,6 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
dc.date,
dc.category,
dc.status,
dc.reject_reason,
dc.created_at,
dc.updated_at,
dc.kandang_id,
@@ -280,7 +265,6 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
Date: row.Date,
Category: row.Category,
Status: row.Status,
RejectReason: row.RejectReason,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
Kandang: kandangMap[row.KandangID],
@@ -361,29 +345,6 @@ func (s dailyChecklistService) GetDetail(c *fiber.Ctx, id uint) (*DailyChecklist
progress = math.Round((float64(completedAssignments) / float64(totalAssignments)) * 100)
}
documentURLs := make([]DailyChecklistDocument, 0)
if s.DocumentSvc != nil {
documents, err := s.DocumentSvc.ListByTarget(c.Context(), string(utils.DocumentTypeDailyChecklist), uint64(id))
if err != nil {
s.Log.Errorf("Failed to list documents for daily checklist %d: %+v", id, err)
return nil, err
}
for _, doc := range documents {
url, err := s.DocumentSvc.PresignURL(c.Context(), doc, 0)
if err != nil {
s.Log.Errorf("Failed to presign document %d for daily checklist %d: %+v", doc.Id, id, err)
continue
}
documentURLs = append(documentURLs, DailyChecklistDocument{
ID: doc.Id,
Name: doc.Name,
Size: doc.Size,
URL: url,
})
}
}
return &DailyChecklistDetail{
Checklist: *checklist,
Phases: phases,
@@ -391,7 +352,6 @@ func (s dailyChecklistService) GetDetail(c *fiber.Ctx, id uint) (*DailyChecklist
AssignedEmployees: assignedEmployees,
TotalActivities: totalActivities,
Progress: progress,
DocumentURLs: documentURLs,
}, nil
}
@@ -417,7 +377,7 @@ func (s *dailyChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create)
err = s.Repository.DB().WithContext(c.Context()).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "date"}, {Name: "kandang_id"}, {Name: "category"}},
DoUpdates: clause.Assignments(map[string]any{"updated_at": time.Now()}),
DoUpdates: clause.Assignments(map[string]any{"status": status, "updated_at": time.Now()}),
}).Create(createBody).Error
if err != nil {
s.Log.Errorf("Failed to upsert dailyChecklist: %+v", err)
@@ -432,22 +392,6 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
return nil, err
}
deletedIDs := make([]uint, 0)
if req.DeletedDocumentIDs != nil {
parts := strings.Split(*req.DeletedDocumentIDs, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
parsedID, err := strconv.ParseUint(part, 10, 64)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid deleted_document_ids")
}
deletedIDs = append(deletedIDs, uint(parsedID))
}
}
updateBody := map[string]any{
"status": req.Status,
}
@@ -456,40 +400,6 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
updateBody["reject_reason"] = *req.RejectReason
}
actorID, err := middleware.ActorIDFromContext(c)
if err != nil {
return &entity.DailyChecklist{}, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
}
if len(deletedIDs) > 0 && s.DocumentSvc != nil {
if err := s.DocumentSvc.DeleteDocuments(c.Context(), deletedIDs, true); err != nil {
s.Log.Errorf("Failed to delete daily checklist documents: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete daily checklist documents")
}
}
if len(req.Documents) > 0 {
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
for idx, file := range req.Documents {
documentFiles = append(documentFiles, commonSvc.DocumentFile{
File: file,
Type: string(utils.DocumentTypeDailyChecklist),
Index: &idx,
})
}
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
DocumentableType: string(utils.DocumentTypeDailyChecklist),
DocumentableID: uint64(id),
CreatedBy: &actorID,
Files: documentFiles,
})
if err != nil {
s.Log.Errorf("Failed to upload daily checklist documents: %+v", err)
return &entity.DailyChecklist{}, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload daily checklist documents")
}
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found")
@@ -959,8 +869,7 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report
Joins("JOIN areas a ON a.id = loc.area_id").
Joins("JOIN phases p ON p.id = dcat.phase_id").
Where("EXTRACT(MONTH FROM dc.date) = ?", params.Month).
Where("EXTRACT(YEAR FROM dc.date) = ?", params.Year).
Where("dc.status = ?", "APPROVED")
Where("EXTRACT(YEAR FROM dc.date) = ?", params.Year)
if params.AreaID != nil {
db = db.Where("a.id = ?", *params.AreaID)
@@ -1,9 +1,5 @@
package validation
import (
"mime/multipart"
)
type Create struct {
Date string `json:"date" validate:"required"`
KandangId uint `json:"kandang_id" validate:"required"`
@@ -12,10 +8,8 @@ type Create struct {
}
type Update struct {
Status string `form:"status" json:"status" validate:"required"`
RejectReason *string `form:"reject_reason" json:"reject_reason"`
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
DeletedDocumentIDs *string `form:"deleted_document_ids" json:"deleted_document_ids"`
Status string `json:"status" validate:"required"`
RejectReason *string `json:"reject_reason"`
}
type Query struct {
@@ -1,206 +0,0 @@
package controller
import (
"math"
"strconv"
"strings"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type DashboardController struct {
DashboardService service.DashboardService
}
func NewDashboardController(dashboardService service.DashboardService) *DashboardController {
return &DashboardController{
DashboardService: dashboardService,
}
}
func (u *DashboardController) GetAll(c *fiber.Ctx) error {
parseStringListParam := func(param string) ([]string, error) {
if param == "" {
return nil, nil
}
parts := strings.Split(param, ",")
result := make([]string, 0, len(parts))
for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed == "" {
return nil, strconv.ErrSyntax
}
result = append(result, trimmed)
}
return result, nil
}
parseUintListParam := func(param string) ([]uint, error) {
if param == "" {
return nil, nil
}
parts := strings.Split(param, ",")
ids := make([]uint, 0, len(parts))
for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed == "" {
return nil, strconv.ErrSyntax
}
parsed, err := strconv.ParseUint(trimmed, 10, 64)
if err != nil {
return nil, err
}
ids = append(ids, uint(parsed))
}
return ids, nil
}
lokasiIds, err := parseUintListParam(c.Query("location_ids", ""))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid location_ids")
}
flockIds, err := parseUintListParam(c.Query("flock_ids", ""))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid flock_ids")
}
kandangIds, err := parseUintListParam(c.Query("kandang_ids", ""))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_ids")
}
include, err := parseStringListParam(strings.ToLower(c.Query("include", "")))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid include")
}
analysisMode := strings.ToUpper(strings.TrimSpace(c.Query("analysis_mode", validation.AnalysisModeOverview)))
metric := strings.ToLower(strings.TrimSpace(c.Query("metric", "")))
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: strings.TrimSpace(c.Query("search", "")),
PerformanceOverviewFilter: validation.PerformanceOverviewFilter{
StartDate: c.Query("start_date", ""),
EndDate: c.Query("end_date", ""),
AnalysisMode: analysisMode,
ComparisonType: strings.ToUpper(strings.TrimSpace(c.Query("comparison_type", ""))),
Metric: metric,
LokasiIds: lokasiIds,
FlockIds: flockIds,
KandangIds: kandangIds,
Include: include,
},
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
if query.AnalysisMode == validation.AnalysisModeComparison && query.ComparisonType == "" {
return fiber.NewError(fiber.StatusBadRequest, "comparison_type is required for comparison mode")
}
location, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
}
startDate, endDate, endExclusive, err := parsePeriodDates(query.StartDate, query.EndDate, location)
if err != nil {
return err
}
query.PeriodStart = startDate
query.PeriodEnd = endDate
query.PeriodEndExclusive = endExclusive
result, totalResults, err := u.DashboardService.GetAll(c.Context(), query)
if err != nil {
return err
}
hasFilter := query.StartDate != "" ||
query.EndDate != "" ||
len(query.LokasiIds) > 0 ||
len(query.FlockIds) > 0 ||
len(query.KandangIds) > 0 ||
len(query.Include) > 0 ||
query.ComparisonType != "" ||
query.Metric != "" ||
query.AnalysisMode != validation.AnalysisModeOverview
var filters interface{}
if hasFilter {
filters = dto.DashboardFiltersDTO{
StartDate: query.StartDate,
EndDate: query.EndDate,
AnalysisMode: query.AnalysisMode,
ComparisonType: query.ComparisonType,
Metric: query.Metric,
LokasiIds: defaultUintSlice(query.LokasiIds),
FlockIds: defaultUintSlice(query.FlockIds),
KandangIds: defaultUintSlice(query.KandangIds),
Include: query.Include,
}
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithMeta{
Code: fiber.StatusOK,
Status: "success",
Message: "Get dashboard successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
Filters: filters,
},
Data: result,
})
}
func defaultUintSlice(values []uint) []uint {
if values == nil {
return []uint{}
}
return values
}
func parsePeriodDates(startDateRaw, endDateRaw string, location *time.Location) (time.Time, time.Time, time.Time, error) {
now := time.Now().In(location)
startDate := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, location)
endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, location)
if startDateRaw != "" {
parsed, err := time.ParseInLocation("2006-01-02", startDateRaw, location)
if err != nil {
return time.Time{}, time.Time{}, time.Time{}, fiber.NewError(fiber.StatusBadRequest, "start_date must follow format YYYY-MM-DD")
}
startDate = parsed
}
if endDateRaw != "" {
parsed, err := time.ParseInLocation("2006-01-02", endDateRaw, location)
if err != nil {
return time.Time{}, time.Time{}, time.Time{}, fiber.NewError(fiber.StatusBadRequest, "end_date must follow format YYYY-MM-DD")
}
endDate = parsed
}
if endDate.Before(startDate) {
return time.Time{}, time.Time{}, time.Time{}, fiber.NewError(fiber.StatusBadRequest, "end_date must be greater than or equal to start_date")
}
endExclusive := endDate.AddDate(0, 0, 1)
return startDate, endDate, endExclusive, nil
}
@@ -1,82 +0,0 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type DashboardListDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type DashboardDetailDTO struct {
DashboardListDTO
}
type DashboardFiltersDTO struct {
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
AnalysisMode string `json:"analysis_mode"`
ComparisonType string `json:"comparison_type,omitempty"`
Metric string `json:"metric,omitempty"`
LokasiIds []uint `json:"location_ids"`
FlockIds []uint `json:"flock_ids"`
KandangIds []uint `json:"kandang_ids"`
Include []string `json:"include,omitempty"`
}
type DashboardStatisticsDTO struct {
Label string `json:"label"`
Value float64 `json:"value"`
PercentLastMonth float64 `json:"percent_last_month"`
}
type DashboardPerformanceOverviewDTO struct {
StatisticsData []DashboardStatisticsDTO `json:"statistics_data"`
Charts map[string]DashboardChartDTO `json:"charts,omitempty"`
}
type DashboardChartSeriesDTO struct {
Id string `json:"id"`
Label string `json:"label"`
Unit string `json:"unit,omitempty"`
}
type DashboardChartDTO struct {
Series []DashboardChartSeriesDTO `json:"series"`
Dataset []map[string]interface{} `json:"dataset"`
}
// === Mapper Functions ===
func ToDashboardListDTO(e entity.Dashboard) DashboardListDTO {
var createdUser *userDTO.UserRelationDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(e.CreatedUser)
createdUser = &mapped
}
return DashboardListDTO{
Id: e.Id,
Name: e.Name,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToDashboardListDTOs(e []entity.Dashboard) []DashboardListDTO {
result := make([]DashboardListDTO, len(e))
for i, r := range e {
result[i] = ToDashboardListDTO(r)
}
return result
}
-26
View File
@@ -1,26 +0,0 @@
package dashboards
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rDashboard "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/repositories"
sDashboard "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type DashboardModule struct{}
func (DashboardModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
dashboardRepo := rDashboard.NewDashboardRepository(db)
userRepo := rUser.NewUserRepository(db)
dashboardService := sDashboard.NewDashboardService(dashboardRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
DashboardRoutes(router, userService, dashboardService)
}
@@ -1,44 +0,0 @@
package repository
import (
"context"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations"
"gorm.io/gorm"
)
type DashboardRepository interface {
repository.BaseRepository[entity.Dashboard]
GetFeedUsageByUom(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]FeedUsageByUom, error)
SumDepletions(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error)
SumInitialPopulation(ctx context.Context, endDate time.Time, filters *validation.DashboardFilter) (float64, error)
SumSapronakCost(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error)
SumBopCost(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error)
SumEkspedisiCost(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error)
SumSellingPrice(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (SellingPriceAggregate, error)
SumEggProductionWeightGrams(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error)
SumEggProductionWeightKg(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error)
GetRecordingWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]RecordingWeeklyMetric, error)
GetUniformityWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]UniformityWeeklyMetric, error)
GetStandardWeeklyMetrics(ctx context.Context, weeks []int, filters *validation.DashboardFilter) ([]StandardWeeklyMetric, error)
GetStandardFcrWeekly(ctx context.Context, weeks []int, filters *validation.DashboardFilter) ([]StandardWeeklyFcrMetric, error)
GetComparisonSeries(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType string) ([]ComparisonSeries, error)
GetComparisonWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType, metric string) ([]ComparisonWeeklyMetric, error)
GetComparisonWeeklyUniformityMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType string) ([]ComparisonUniformityMetric, error)
GetEggQualityWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]EggQualityWeeklyMetric, error)
GetEggWeightWeeklyGrams(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]WeeklyEggWeightMetric, error)
GetFeedUsageWeeklyByUom(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]WeeklyFeedUsageMetric, error)
}
type DashboardRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Dashboard]
}
func NewDashboardRepository(db *gorm.DB) DashboardRepository {
return &DashboardRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Dashboard](db),
}
}
@@ -1,725 +0,0 @@
package repository
import (
"context"
"fmt"
"strings"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
)
type SellingPriceAggregate struct {
TotalPrice float64
TotalWeight float64
}
type FeedUsageByUom struct {
TotalQty float64
UomName string
}
type RecordingWeeklyMetric struct {
Week int
HenDay float64
EggWeight float64
FeedIntake float64
FcrValue float64
CumDepletionRate float64
}
type UniformityWeeklyMetric struct {
Week int
Uniformity float64
AverageWeight float64
}
type StandardWeeklyMetric struct {
Week int
StdLaying float64
StdEggWeight float64
StdFeedIntake float64
StdUniformity float64
StdDepletion float64
StdBodyWeight float64
}
type StandardWeeklyFcrMetric struct {
Week int
StdFcr float64
}
type ComparisonSeries struct {
Id uint
Label string
}
type ComparisonWeeklyMetric struct {
Week int
SeriesId uint
Value float64
}
type ComparisonUniformityMetric struct {
Week int
SeriesId uint
Uniformity float64
AverageWeight float64
}
type EggQualityWeeklyMetric struct {
Week int
NormalQty float64
AbnormalQty float64
TotalQty float64
}
type WeeklyEggWeightMetric struct {
Week int
EggWeightGrams float64
}
type WeeklyFeedUsageMetric struct {
Week int
TotalQty float64
UomName string
}
func applyDashboardFilters(db *gorm.DB, filters *validation.DashboardFilter) *gorm.DB {
if filters == nil {
return db
}
if len(filters.FlockIds) > 0 {
db = db.Where("pfk.project_flock_id IN ?", filters.FlockIds)
}
if len(filters.KandangIds) > 0 {
db = db.Where("k.id IN ?", filters.KandangIds)
}
if len(filters.LokasiIds) > 0 {
db = db.Where("k.location_id IN ?", filters.LokasiIds)
}
return db
}
func (r *DashboardRepositoryImpl) GetRecordingWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]RecordingWeeklyMetric, error) {
var rows []RecordingWeeklyMetric
db := r.DB().WithContext(ctx).
Table("recordings AS r").
Select(`((r.day - 1) / 7 + 1) AS week,
COALESCE(AVG(r.hen_day), 0) AS hen_day,
COALESCE(AVG(r.egg_weight), 0) AS egg_weight,
COALESCE(AVG(r.feed_intake), 0) AS feed_intake,
COALESCE(AVG(r.fcr_value), 0) AS fcr_value,
COALESCE(AVG(r.cum_depletion_rate), 0) AS cum_depletion_rate`).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL").
Where("r.day IS NOT NULL AND r.day > 0")
db = applyDashboardFilters(db, filters)
if err := db.Group("week").Order("week ASC").Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *DashboardRepositoryImpl) GetUniformityWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]UniformityWeeklyMetric, error) {
var rows []UniformityWeeklyMetric
db := r.DB().WithContext(ctx).
Table("project_flock_kandang_uniformity AS u").
Select(`u.week AS week,
COALESCE(AVG(u.uniformity), 0) AS uniformity,
COALESCE(AVG((u.chart_data->'statistics'->>'average_weight')::numeric), 0) AS average_weight`).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = u.project_flock_kandang_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Where("u.uniform_date IS NOT NULL").
Where("u.uniform_date >= ? AND u.uniform_date < ?", start, end)
db = applyDashboardFilters(db, filters)
if err := db.Group("u.week").Order("u.week ASC").Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *DashboardRepositoryImpl) GetStandardWeeklyMetrics(ctx context.Context, weeks []int, filters *validation.DashboardFilter) ([]StandardWeeklyMetric, error) {
if len(weeks) == 0 {
return nil, nil
}
standardIDs := r.standardIDSubquery(filters)
if standardIDs == nil {
return nil, nil
}
var rows []StandardWeeklyMetric
db := r.DB().WithContext(ctx).
Table("standard_growth_details AS sgd").
Select(`sgd.week AS week,
COALESCE(AVG(psd.target_hen_day_production), 0) AS std_laying,
COALESCE(AVG(psd.target_egg_weight), 0) AS std_egg_weight,
COALESCE(AVG(sgd.feed_intake), 0) AS std_feed_intake,
COALESCE(AVG(sgd.min_uniformity), 0) AS std_uniformity,
COALESCE(AVG(sgd.max_depletion), 0) AS std_depletion,
COALESCE(AVG(sgd.target_mean_bw), 0) AS std_body_weight`).
Joins("LEFT JOIN production_standard_details AS psd ON psd.production_standard_id = sgd.production_standard_id AND psd.week = sgd.week").
Where("sgd.week IN ?", weeks).
Where("sgd.production_standard_id IN (?)", standardIDs)
if err := db.Group("sgd.week").Order("sgd.week ASC").Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *DashboardRepositoryImpl) GetStandardFcrWeekly(ctx context.Context, weeks []int, filters *validation.DashboardFilter) ([]StandardWeeklyFcrMetric, error) {
if len(weeks) == 0 {
return nil, nil
}
filterClause := ""
filterArgs := make([]interface{}, 0)
if filters != nil {
if len(filters.FlockIds) > 0 {
filterClause += " AND pf.id IN ?"
filterArgs = append(filterArgs, filters.FlockIds)
}
if len(filters.KandangIds) > 0 {
filterClause += " AND k.id IN ?"
filterArgs = append(filterArgs, filters.KandangIds)
}
if len(filters.LokasiIds) > 0 {
filterClause += " AND k.location_id IN ?"
filterArgs = append(filterArgs, filters.LokasiIds)
}
}
query := fmt.Sprintf(`
WITH src AS (
SELECT DISTINCT pf.production_standard_id, pf.fcr_id
FROM project_flocks pf
JOIN project_flock_kandangs pfk ON pfk.project_flock_id = pf.id
JOIN kandangs k ON k.id = pfk.kandang_id
WHERE pf.production_standard_id > 0 AND pf.fcr_id > 0
%s
),
actual AS (
SELECT u.week AS week,
pf.fcr_id AS fcr_id,
AVG((u.chart_data->'statistics'->>'average_weight')::numeric) AS avg_weight
FROM project_flock_kandang_uniformity u
JOIN project_flock_kandangs pfk ON pfk.id = u.project_flock_kandang_id
JOIN project_flocks pf ON pf.id = pfk.project_flock_id
JOIN kandangs k ON k.id = pfk.kandang_id
WHERE u.week IN ? AND u.uniform_date IS NOT NULL AND pf.fcr_id > 0
%s
GROUP BY u.week, pf.fcr_id
),
target AS (
SELECT sgd.week AS week,
src.fcr_id AS fcr_id,
AVG(sgd.target_mean_bw) AS target_mean_bw
FROM standard_growth_details sgd
JOIN src ON src.production_standard_id = sgd.production_standard_id
WHERE sgd.week IN ?
GROUP BY sgd.week, src.fcr_id
),
weights AS (
SELECT COALESCE(a.week, t.week) AS week,
COALESCE(a.fcr_id, t.fcr_id) AS fcr_id,
COALESCE(
CASE WHEN a.avg_weight > 10 THEN a.avg_weight / 1000 ELSE a.avg_weight END,
CASE WHEN t.target_mean_bw > 10 THEN t.target_mean_bw / 1000 ELSE t.target_mean_bw END
) AS weight
FROM actual a
FULL OUTER JOIN target t ON t.week = a.week AND t.fcr_id = a.fcr_id
)
SELECT w.week AS week,
COALESCE(AVG(
COALESCE(
(SELECT fs.fcr_number
FROM fcr_standards fs
WHERE fs.fcr_id = w.fcr_id
AND fs.weight >= w.weight
ORDER BY fs.weight ASC
LIMIT 1),
(SELECT fs.fcr_number
FROM fcr_standards fs
WHERE fs.fcr_id = w.fcr_id
ORDER BY fs.weight DESC
LIMIT 1)
)
), 0) AS std_fcr
FROM weights w
GROUP BY w.week
ORDER BY w.week ASC
`, filterClause, filterClause)
args := make([]interface{}, 0, len(filterArgs)*2+2)
args = append(args, filterArgs...)
args = append(args, weeks)
args = append(args, filterArgs...)
args = append(args, weeks)
var rows []StandardWeeklyFcrMetric
if err := r.DB().WithContext(ctx).Raw(query, args...).Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *DashboardRepositoryImpl) SumEggProductionWeightGrams(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error) {
var total float64
db := r.DB().WithContext(ctx).
Table("recording_eggs AS re").
Select("COALESCE(SUM(re.qty * re.weight), 0)").
Joins("JOIN recordings AS r ON r.id = re.recording_id").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL")
db = applyDashboardFilters(db, filters)
if err := db.Scan(&total).Error; err != nil {
return 0, err
}
return total, nil
}
func (r *DashboardRepositoryImpl) SumEggProductionWeightKg(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error) {
grams, err := r.SumEggProductionWeightGrams(ctx, start, end, filters)
if err != nil {
return 0, err
}
return grams / 1000, nil
}
func (r *DashboardRepositoryImpl) GetFeedUsageByUom(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]FeedUsageByUom, error) {
var rows []FeedUsageByUom
db := r.DB().WithContext(ctx).
Table("recording_stocks AS rs").
Select("COALESCE(SUM(rs.usage_qty), 0) + COALESCE(SUM(rs.pending_qty), 0) AS total_qty, LOWER(uoms.name) AS uom_name").
Joins("JOIN recordings AS r ON r.id = rs.recording_id").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
Joins("JOIN products AS p ON p.id = pw.product_id").
Joins("JOIN uoms ON uoms.id = p.uom_id").
Joins("JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ? AND UPPER(f.name) = ?", entity.FlagableTypeProduct, "PAKAN").
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL")
db = applyDashboardFilters(db, filters)
if err := db.Group("LOWER(uoms.name)").Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *DashboardRepositoryImpl) SumDepletions(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error) {
var total float64
db := r.DB().WithContext(ctx).
Table("recording_depletions AS rd").
Select("COALESCE(SUM(rd.qty), 0)").
Joins("JOIN recordings AS r ON r.id = rd.recording_id").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL")
db = applyDashboardFilters(db, filters)
if err := db.Scan(&total).Error; err != nil {
return 0, err
}
return total, nil
}
func (r *DashboardRepositoryImpl) SumInitialPopulation(ctx context.Context, endDate time.Time, filters *validation.DashboardFilter) (float64, error) {
var total float64
endOfDate := endDate.AddDate(0, 0, 1)
db := r.DB().WithContext(ctx).
Table("project_chickins AS pc").
Select("COALESCE(SUM(pc.usage_qty), 0)").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = pc.project_flock_kandang_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Where("pc.chick_in_date < ?", endOfDate).
Where("pc.deleted_at IS NULL")
db = applyDashboardFilters(db, filters)
if err := db.Scan(&total).Error; err != nil {
return 0, err
}
return total, nil
}
func (r *DashboardRepositoryImpl) SumSellingPrice(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (SellingPriceAggregate, error) {
var result SellingPriceAggregate
db := r.DB().WithContext(ctx).
Table("marketing_delivery_products AS mdp").
Select("COALESCE(SUM(mdp.total_price), 0) AS total_price, COALESCE(SUM(mdp.total_weight), 0) AS total_weight").
Joins("JOIN marketing_products AS mp ON mp.id = mdp.marketing_product_id").
Joins("JOIN product_warehouses AS pw ON pw.id = mp.product_warehouse_id").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = pw.project_flock_kandang_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Where("mdp.delivery_date IS NOT NULL").
Where("mdp.delivery_date >= ? AND mdp.delivery_date < ?", start, end)
db = applyDashboardFilters(db, filters)
if err := db.Scan(&result).Error; err != nil {
return SellingPriceAggregate{}, err
}
return result, nil
}
func (r *DashboardRepositoryImpl) SumSapronakCost(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error) {
var total float64
db := r.DB().WithContext(ctx).
Table("purchase_items AS pi").
Select("COALESCE(SUM(pi.total_price), 0) AS total").
Joins("JOIN products AS p ON p.id = pi.product_id").
Joins("JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Joins("LEFT JOIN product_warehouses AS pw ON pw.id = pi.product_warehouse_id").
Joins("LEFT JOIN project_flock_kandangs AS pfk ON pfk.id = COALESCE(pi.project_flock_kandang_id, pw.project_flock_kandang_id)").
Joins("LEFT JOIN kandangs AS k ON k.id = pfk.kandang_id").
Where("f.name IN ?", []utils.FlagType{utils.FlagDOC, utils.FlagPakan, utils.FlagOVK}).
Where("pi.received_date IS NOT NULL").
Where("pi.received_date >= ? AND pi.received_date < ?", start, end)
db = applyDashboardFilters(db, filters)
if err := db.Scan(&total).Error; err != nil {
return 0, err
}
return total, nil
}
func (r *DashboardRepositoryImpl) SumBopCost(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error) {
return r.sumExpenseRealization(ctx, start, end, filters, func(db *gorm.DB) *gorm.DB {
return db.
Where("e.category = ?", utils.ExpenseCategoryBOP).
Joins("LEFT JOIN nonstocks AS n ON n.id = en.nonstock_id").
Joins("LEFT JOIN flags AS f ON f.flagable_id = n.id AND f.flagable_type = ? AND f.name = ?", entity.FlagableTypeNonstock, utils.FlagEkspedisi).
Where("f.id IS NULL")
})
}
func (r *DashboardRepositoryImpl) SumEkspedisiCost(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) (float64, error) {
return r.sumExpenseRealization(ctx, start, end, filters, func(db *gorm.DB) *gorm.DB {
return db.
Joins("JOIN nonstocks AS n ON n.id = en.nonstock_id").
Joins("JOIN flags AS f ON f.flagable_id = n.id AND f.flagable_type = ?", entity.FlagableTypeNonstock).
Where("f.name = ?", utils.FlagEkspedisi)
})
}
func (r *DashboardRepositoryImpl) sumExpenseRealization(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, modifier func(*gorm.DB) *gorm.DB) (float64, error) {
var total float64
db := r.DB().WithContext(ctx).
Table("expense_realizations AS er").
Select("COALESCE(SUM(er.qty * er.price), 0) AS total").
Joins("JOIN expense_nonstocks AS en ON en.id = er.expense_nonstock_id").
Joins("JOIN expenses AS e ON e.id = en.expense_id").
Joins("LEFT JOIN project_flock_kandangs AS pfk ON pfk.id = en.project_flock_kandang_id").
Joins("LEFT JOIN kandangs AS k ON k.id = COALESCE(en.kandang_id, pfk.kandang_id)").
Where("e.realization_date >= ? AND e.realization_date < ?", start, end)
db = applyDashboardFilters(db, filters)
if modifier != nil {
db = modifier(db)
}
if err := db.Scan(&total).Error; err != nil {
return 0, err
}
return total, nil
}
func (r *DashboardRepositoryImpl) standardIDSubquery(filters *validation.DashboardFilter) *gorm.DB {
db := r.DB().
Table("project_flocks AS pf").
Select("DISTINCT pf.production_standard_id").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Where("pf.production_standard_id > 0")
if filters != nil {
if len(filters.FlockIds) > 0 {
db = db.Where("pf.id IN ?", filters.FlockIds)
}
if len(filters.KandangIds) > 0 {
db = db.Where("k.id IN ?", filters.KandangIds)
}
if len(filters.LokasiIds) > 0 {
db = db.Where("k.location_id IN ?", filters.LokasiIds)
}
}
return db
}
func (r *DashboardRepositoryImpl) standardSourceSubquery(filters *validation.DashboardFilter) *gorm.DB {
db := r.DB().
Table("project_flocks AS pf").
Select("DISTINCT pf.production_standard_id, pf.fcr_id").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Where("pf.production_standard_id > 0").
Where("pf.fcr_id > 0")
if filters != nil {
if len(filters.FlockIds) > 0 {
db = db.Where("pf.id IN ?", filters.FlockIds)
}
if len(filters.KandangIds) > 0 {
db = db.Where("k.id IN ?", filters.KandangIds)
}
if len(filters.LokasiIds) > 0 {
db = db.Where("k.location_id IN ?", filters.LokasiIds)
}
}
return db
}
func (r *DashboardRepositoryImpl) GetComparisonSeries(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType string) ([]ComparisonSeries, error) {
seriesExpr, labelExpr, groupExpr, orderExpr, err := comparisonSeriesColumns(comparisonType)
if err != nil {
return nil, err
}
var rows []ComparisonSeries
db := r.DB().WithContext(ctx).
Table("recordings AS r").
Select(fmt.Sprintf("%s AS id, %s AS label", seriesExpr, labelExpr)).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id").
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL")
db = applyDashboardFilters(db, filters)
if err := db.Group(groupExpr).Order(orderExpr).Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *DashboardRepositoryImpl) GetComparisonWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType, metric string) ([]ComparisonWeeklyMetric, error) {
seriesExpr, _, groupExpr, orderExpr, err := comparisonSeriesColumns(comparisonType)
if err != nil {
return nil, err
}
metricExpr, err := comparisonMetricColumn(metric)
if err != nil {
return nil, err
}
var rows []ComparisonWeeklyMetric
db := r.DB().WithContext(ctx).
Table("recordings AS r").
Select(fmt.Sprintf(`((r.day - 1) / 7 + 1) AS week,
%s AS series_id,
COALESCE(AVG(%s), 0) AS value`, seriesExpr, metricExpr)).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id").
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL").
Where("r.day IS NOT NULL AND r.day > 0")
db = applyDashboardFilters(db, filters)
groupBy := fmt.Sprintf("week, %s", groupExpr)
orderBy := fmt.Sprintf("week ASC, %s", orderExpr)
if err := db.Group(groupBy).Order(orderBy).Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *DashboardRepositoryImpl) GetComparisonWeeklyUniformityMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType string) ([]ComparisonUniformityMetric, error) {
seriesExpr, _, groupExpr, orderExpr, err := comparisonSeriesColumns(comparisonType)
if err != nil {
return nil, err
}
var rows []ComparisonUniformityMetric
db := r.DB().WithContext(ctx).
Table("project_flock_kandang_uniformity AS u").
Select(fmt.Sprintf(`u.week AS week,
%s AS series_id,
COALESCE(AVG(u.uniformity), 0) AS uniformity,
COALESCE(AVG((u.chart_data->'statistics'->>'average_weight')::numeric), 0) AS average_weight`, seriesExpr)).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = u.project_flock_kandang_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id").
Where("u.uniform_date IS NOT NULL").
Where("u.uniform_date >= ? AND u.uniform_date < ?", start, end)
db = applyDashboardFilters(db, filters)
groupBy := fmt.Sprintf("u.week, %s", groupExpr)
orderBy := fmt.Sprintf("u.week ASC, %s", orderExpr)
if err := db.Group(groupBy).Order(orderBy).Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *DashboardRepositoryImpl) GetEggQualityWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]EggQualityWeeklyMetric, error) {
var rows []EggQualityWeeklyMetric
db := r.DB().WithContext(ctx).
Table("recording_eggs AS re").
Select(`
((r.day - 1) / 7 + 1) AS week,
COALESCE(SUM(CASE WHEN f.name = ? THEN re.qty ELSE 0 END), 0) AS normal_qty,
COALESCE(SUM(CASE WHEN f.name IN (?, ?, ?) THEN re.qty ELSE 0 END), 0) AS abnormal_qty,
COALESCE(SUM(re.qty), 0) AS total_qty`,
utils.FlagTelurUtuh,
utils.FlagTelurPutih,
utils.FlagTelurRetak,
utils.FlagTelurPecah,
).
Joins("JOIN recordings AS r ON r.id = re.recording_id").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN product_warehouses AS pw ON pw.id = re.product_warehouse_id").
Joins("JOIN products AS p ON p.id = pw.product_id").
Joins("JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where("f.name IN ?", []utils.FlagType{utils.FlagTelurUtuh, utils.FlagTelurPutih, utils.FlagTelurRetak, utils.FlagTelurPecah}).
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL").
Where("r.day IS NOT NULL AND r.day > 0")
db = applyDashboardFilters(db, filters)
if err := db.Group("week").Order("week ASC").Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *DashboardRepositoryImpl) GetEggWeightWeeklyGrams(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]WeeklyEggWeightMetric, error) {
var rows []WeeklyEggWeightMetric
db := r.DB().WithContext(ctx).
Table("recording_eggs AS re").
Select(`
((r.day - 1) / 7 + 1) AS week,
COALESCE(SUM(re.qty * re.weight), 0) AS egg_weight_grams`).
Joins("JOIN recordings AS r ON r.id = re.recording_id").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL").
Where("r.day IS NOT NULL AND r.day > 0")
db = applyDashboardFilters(db, filters)
if err := db.Group("week").Order("week ASC").Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *DashboardRepositoryImpl) GetFeedUsageWeeklyByUom(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]WeeklyFeedUsageMetric, error) {
var rows []WeeklyFeedUsageMetric
db := r.DB().WithContext(ctx).
Table("recording_stocks AS rs").
Select(`
((r.day - 1) / 7 + 1) AS week,
COALESCE(SUM(rs.usage_qty), 0) + COALESCE(SUM(rs.pending_qty), 0) AS total_qty,
LOWER(uoms.name) AS uom_name`).
Joins("JOIN recordings AS r ON r.id = rs.recording_id").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
Joins("JOIN products AS p ON p.id = pw.product_id").
Joins("JOIN uoms ON uoms.id = p.uom_id").
Joins("JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ? AND UPPER(f.name) = ?", entity.FlagableTypeProduct, "PAKAN").
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL").
Where("r.day IS NOT NULL AND r.day > 0")
db = applyDashboardFilters(db, filters)
if err := db.Group("week, LOWER(uoms.name)").Order("week ASC").Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func comparisonSeriesColumns(comparisonType string) (string, string, string, string, error) {
switch strings.ToUpper(strings.TrimSpace(comparisonType)) {
case validation.ComparisonTypeFarm:
return "loc.id", "loc.name", "loc.id, loc.name", "loc.name", nil
case validation.ComparisonTypeFlock:
return "pf.id", "pf.flock_name", "pf.id, pf.flock_name", "pf.flock_name", nil
case validation.ComparisonTypeKandang:
return "k.id", "k.name", "k.id, k.name", "k.name", nil
default:
return "", "", "", "", fmt.Errorf("invalid comparison_type")
}
}
func comparisonMetricColumn(metric string) (string, error) {
switch strings.ToLower(strings.TrimSpace(metric)) {
case validation.MetricFcr:
return "r.fcr_value", nil
case validation.MetricMortality:
return "r.cum_depletion_rate", nil
case validation.MetricLaying:
return "r.hen_day", nil
case validation.MetricEggWeight:
return "r.egg_weight", nil
case validation.MetricFeedIntake:
return "r.feed_intake", nil
default:
return "", fmt.Errorf("invalid metric")
}
}
-18
View File
@@ -1,18 +0,0 @@
package dashboards
import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/controllers"
dashboard "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func DashboardRoutes(v1 fiber.Router, u user.UserService, s dashboard.DashboardService) {
ctrl := controller.NewDashboardController(s)
route := v1.Group("/dashboards")
route.Use(m.Auth(u))
route.Get("/",m.RequirePermissions(m.P_DashboardGetAll) ,ctrl.GetAll)
}
File diff suppressed because it is too large Load Diff
@@ -1,54 +0,0 @@
package validation
import "time"
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
}
const (
AnalysisModeOverview = "OVERVIEW"
AnalysisModeComparison = "COMPARISON"
ComparisonTypeFarm = "FARM"
ComparisonTypeFlock = "FLOCK"
ComparisonTypeKandang = "KANDANG"
MetricFcr = "fcr"
MetricMortality = "mortality"
MetricLaying = "laying"
MetricEggWeight = "egg_weight"
MetricFeedIntake = "feed_intake"
)
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
PerformanceOverviewFilter
PeriodStart time.Time `json:"-" query:"-"`
PeriodEnd time.Time `json:"-" query:"-"`
PeriodEndExclusive time.Time `json:"-" query:"-"`
}
type PerformanceOverviewFilter struct {
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
AnalysisMode string `query:"analysis_mode" validate:"omitempty,oneof=OVERVIEW COMPARISON"`
ComparisonType string `query:"comparison_type" validate:"omitempty,oneof=FARM FLOCK KANDANG"`
Metric string `query:"metric" validate:"omitempty,oneof=fcr mortality laying egg_weight feed_intake"`
LokasiIds []uint `query:"location_ids" validate:"omitempty,dive,gt=0"`
FlockIds []uint `query:"flock_ids" validate:"omitempty,dive,gt=0"`
KandangIds []uint `query:"kandang_ids" validate:"omitempty,dive,gt=0"`
Include []string `query:"include" validate:"omitempty,dive,oneof=statistics charts"`
}
type DashboardFilter struct {
LokasiIds []uint
FlockIds []uint
KandangIds []uint
}
@@ -229,12 +229,10 @@ func (u *ExpenseController) Approval(c *fiber.Ctx) error {
path := c.Path()
approvalType := ""
if strings.Contains(path, "/approvals/head-area") {
approvalType = "head-area"
if strings.Contains(path, "/approvals/manager") {
approvalType = "manager"
} else if strings.Contains(path, "/approvals/finance") {
approvalType = "finance"
} else if strings.Contains(path, "/approvals/unit-vice-president") {
approvalType = "unit-vice-president"
} else {
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval path")
}
@@ -2,7 +2,6 @@ package repository
import (
"context"
"fmt"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -16,7 +15,6 @@ type ExpenseRealizationRepository interface {
IdExists(ctx context.Context, id uint64) (bool, error)
GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error)
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error)
GetClosingOverhead(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.ExpenseRealization, error)
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error)
}
@@ -57,40 +55,6 @@ func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Conte
return realizations, err
}
func (r *ExpenseRealizationRepositoryImpl) GetClosingOverhead(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.ExpenseRealization, error) {
var realizations []entity.ExpenseRealization
db := r.DB().WithContext(ctx).
Preload("ExpenseNonstock").
Preload("ExpenseNonstock.Nonstock").
Preload("ExpenseNonstock.Nonstock.Uom").
Preload("ExpenseNonstock.Nonstock.Flags").
Preload("ExpenseNonstock.Expense").
Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id").
Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id").
Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id").
Joins("LEFT JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id").
Where("expenses.realization_date IS NOT NULL")
if projectFlockKandangID != nil {
db = db.Where(`(
expense_nonstocks.project_flock_kandang_id = ? OR
(expense_nonstocks.kandang_id = (SELECT kandang_id FROM project_flock_kandangs WHERE id = ?) AND
expense_nonstocks.project_flock_kandang_id IS NULL) OR
(expenses.project_flock_id IS NOT NULL AND expenses.project_flock_id::jsonb @> ?::jsonb)
)`, *projectFlockKandangID, *projectFlockKandangID, fmt.Sprintf("[%d]", projectFlockID))
} else {
db = db.Where(`(
project_flock_kandangs.project_flock_id = ? OR
kandangs.id IN (SELECT kandang_id FROM project_flock_kandangs WHERE project_flock_id = ?) OR
(expenses.project_flock_id IS NOT NULL AND expenses.project_flock_id::jsonb @> ?::jsonb)
)`, projectFlockID, projectFlockID, fmt.Sprintf("[%d]", projectFlockID))
}
err := db.Find(&realizations).Error
return realizations, err
}
func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error) {
var realizations []entity.ExpenseRealization
var total int64
@@ -111,7 +75,7 @@ func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context
Joins("LEFT JOIN suppliers ON suppliers.id = expenses.supplier_id")
if filters.Search != "" {
db = db.Where("expenses.category ILIKE ? OR expenses.reference_number ILIKE ? OR expenses.po_number ILIKE ? OR expenses.notes ILIKE ? OR suppliers.name ILIKE ?",
db = db.Where("LOWER(expenses.category) LIKE LOWER(?) OR LOWER(expenses.reference_number) LIKE LOWER(?) OR LOWER(expenses.po_number) LIKE LOWER(?) OR LOWER(expenses.notes) LIKE LOWER(?) OR LOWER(suppliers.name) LIKE LOWER(?)",
"%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%")
}
+1 -4
View File
@@ -27,11 +27,8 @@ func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService
route.Get("/:id", m.RequirePermissions(m.P_ExpenseGetOne), ctrl.GetOne)
route.Patch("/:id", m.RequirePermissions(m.P_ExpenseUpdateOne), ctrl.UpdateOne)
route.Delete("/:id", m.RequirePermissions(m.P_ExpenseDeleteOne), ctrl.DeleteOne)
route.Post("/approvals/head-area", m.RequirePermissions(m.P_ExpenseApprovalHeadArea), ctrl.Approval)
route.Post("/approvals/manager", m.RequirePermissions(m.P_ExpenseApprovalManager), ctrl.Approval)
route.Post("/approvals/finance", m.RequirePermissions(m.P_ExpenseApprovalFinance), ctrl.Approval)
route.Post("/approvals/unit-vice-president", m.RequirePermissions(m.P_ExpenseApprovalUnitVicePresident), ctrl.Approval)
route.Post("/:id/realizations", m.RequirePermissions(m.P_ExpenseCreateRealizations), ctrl.CreateRealization)
route.Patch("/:id/realizations", m.RequirePermissions(m.P_ExpenseUpdateRealizations), ctrl.UpdateRealization)
route.Post("/:id/complete", m.RequirePermissions(m.P_ExpenseCompleteExpense), ctrl.CompleteExpense)
@@ -92,7 +92,7 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expens
expenses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("category ILIKE ?", "%"+params.Search+"%")
return db.Where("LOWER(category) LIKE LOWER(?)", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
@@ -1049,30 +1049,21 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
}
var stepNumber approvalutils.ApprovalStep
if approvalType == "head-area" {
if approvalType == "manager" {
stepNumber = utils.ExpenseStepHeadArea
stepNumber = utils.ExpenseStepManager
if latestApproval.StepNumber != uint16(utils.ExpenseStepPengajuan) {
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
return fiber.NewError(fiber.StatusBadRequest,
fmt.Sprintf("Cannot process at Head Area step. Latest approval is at %s step. Expected previous step: Pengajuan", currentStepName))
fmt.Sprintf("Cannot process at Manager step. Latest approval is at %s step. Expected previous step: Pengajuan", currentStepName))
}
} else if approvalType == "unit-vice-president" {
stepNumber = utils.ExpenseStepUnitVicePresident
if latestApproval.StepNumber != uint16(utils.ExpenseStepHeadArea) {
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
return fiber.NewError(fiber.StatusBadRequest,
fmt.Sprintf("Cannot process at Unit Vice President step. Latest approval is at %s step. Expected previous step: Head Area", currentStepName))
}
} else if approvalType == "finance" {
stepNumber = utils.ExpenseStepFinance
if latestApproval.StepNumber != uint16(utils.ExpenseStepUnitVicePresident) {
if latestApproval.StepNumber != uint16(utils.ExpenseStepManager) {
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
return fiber.NewError(fiber.StatusBadRequest,
fmt.Sprintf("Cannot process at Finance step. Latest approval is at %s step. Expected previous step: Unit Vice President", currentStepName))
fmt.Sprintf("Cannot process at Finance step. Latest approval is at %s step. Expected previous step: Manager", currentStepName))
}
} else {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid approval type: %v", approvalType))
@@ -399,11 +399,11 @@ func (r *ProductWarehouseRepositoryImpl) ListProductIDsByFlagPrefixes(ctx contex
}
like := prefix + "%"
if !applied {
db = db.Where("flags.name ILIKE ?", like)
db = db.Where("LOWER(flags.name) LIKE LOWER(?)", like)
applied = true
continue
}
db = db.Or("flags.name ILIKE ?", like)
db = db.Or("LOWER(flags.name) LIKE LOWER(?)", like)
}
if visibleStatus != nil {
@@ -99,7 +99,7 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
db = db.Where("movement_number ILIKE ?", "%"+strings.TrimSpace(params.Search)+"%")
db = db.Where("LOWER(movement_number) LIKE LOWER(?)", "%"+strings.TrimSpace(params.Search)+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
@@ -39,12 +39,12 @@ type TransferExpenseReceivingPayload struct {
}
type groupedTransferItem struct {
detail *entity.StockTransferDetail
payload TransferExpenseReceivingPayload
projectFK *uint
kandangID *uint
totalPrice float64
shippingCostTotal float64
detail *entity.StockTransferDetail
payload TransferExpenseReceivingPayload
projectFK *uint
kandangID *uint
totalPrice float64
shippingCostTotal float64
}
func groupingKey(supplierID uint, date time.Time, warehouseID uint) string {
@@ -84,6 +84,7 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it
expenseIDs := make(map[uint64]struct{})
expenseNonstockIDs := make([]uint64, 0)
for _, item := range items {
if item.ExpenseNonstockId != nil && *item.ExpenseNonstockId != 0 {
expenseNonstockIDs = append(expenseNonstockIDs, *item.ExpenseNonstockId)
@@ -91,7 +92,7 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it
}
if len(expenseNonstockIDs) > 0 {
for _, nsID := range expenseNonstockIDs {
var expenseID uint64
if err := tx.Model(&entity.ExpenseNonstock{}).
@@ -105,11 +106,13 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it
}
}
if err := tx.Delete(&entity.ExpenseNonstock{}, expenseNonstockIDs).Error; err != nil {
return err
}
}
approvalRepoTx := commonRepo.NewApprovalRepository(tx)
for expenseID := range expenseIDs {
var count int64
@@ -119,6 +122,7 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it
return err
}
if count == 0 {
if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(expenseID)); err != nil {
return err
@@ -216,6 +220,7 @@ func (b *transferExpenseBridge) createExpenseViaService(
for _, gi := range items {
note := fmt.Sprintf("stock_transfer_detail:%d", gi.detail.Id)
price := gi.shippingCostTotal
if gi.payload.TransportPerItem != nil {
price = *gi.payload.TransportPerItem * gi.payload.DeliveredQty
@@ -223,7 +228,7 @@ func (b *transferExpenseBridge) createExpenseViaService(
costItems = append(costItems, expenseValidation.CostItem{
NonstockID: expeditionNonstockID,
Quantity: 1,
Quantity: 1,
Price: price,
Notes: note,
})
@@ -246,16 +251,14 @@ func (b *transferExpenseBridge) createExpenseViaService(
return nil, err
}
action := entity.ApprovalActionApproved
actorID := uint(transfer.CreatedBy)
if actorID == 0 {
actorID = 1
}
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepHeadArea, &action, actorID, nil); err != nil {
return nil, err
}
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepUnitVicePresident, &action, actorID, nil); err != nil {
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepManager, &action, actorID, nil); err != nil {
return nil, err
}
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
@@ -325,6 +328,7 @@ func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64
ctx := c.Context()
transfer, err := b.transferRepo.GetByID(ctx, uint(transferID), func(db *gorm.DB) *gorm.DB {
return db.
Preload("Details").
@@ -344,10 +348,11 @@ func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64
for i := range transfer.Details {
detailMap[transfer.Details[i].Id] = &transfer.Details[i]
for _, deliveryItem := range transfer.Details[i].DeliveryItems {
if deliveryItem.StockTransferDelivery != nil {
shippingCostMap[transfer.Details[i].Id] = deliveryItem.StockTransferDelivery.ShippingCostTotal
break
break
}
}
}
@@ -390,14 +395,17 @@ func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64
}
}
shippingCostTotal := shippingCostMap[detail.Id]
totalPrice := shippingCostTotal
if payload.TransportPerItem != nil {
totalPrice = *payload.TransportPerItem * payload.DeliveredQty
}
warehouseID := uint(payload.WarehouseID)
if warehouseID == 0 && transfer.ToWarehouse != nil {
warehouseID = uint(transfer.ToWarehouse.Id)
@@ -14,7 +14,6 @@ import (
type MarketingDeliveryProductRepository interface {
repository.BaseRepository[entity.MarketingDeliveryProduct]
GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error)
GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error)
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.MarketingQuery) ([]entity.MarketingDeliveryProduct, int64, error)
@@ -54,43 +53,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetDeliveryProductsByProjectFlo
return deliveryProducts, nil
}
func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) {
var deliveryProducts []entity.MarketingDeliveryProduct
db := r.DB().WithContext(ctx).
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id").
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
Where("marketing_delivery_products.delivery_date IS NOT NULL").
Distinct("marketing_delivery_products.*")
if projectFlockKandangID != nil {
db = db.Where("product_warehouses.project_flock_kandang_id = ?", *projectFlockKandangID)
}
db = db.
Preload("MarketingProduct").
Preload("MarketingProduct.ProductWarehouse").
Preload("MarketingProduct.ProductWarehouse.Product").
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
Preload("MarketingProduct.ProductWarehouse.Product.Uom").
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
Preload("MarketingProduct.ProductWarehouse.Warehouse").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins").
Preload("MarketingProduct.Marketing").
Preload("MarketingProduct.Marketing.Customer").
Order("marketing_delivery_products.delivery_date DESC")
if err := db.Find(&deliveryProducts).Error; err != nil {
return nil, err
}
return deliveryProducts, nil
}
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) {
var deliveryProducts []entity.MarketingDeliveryProduct
@@ -137,14 +99,13 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
Preload("ProductWarehouse.ProjectFlockKandang.ProjectFlock")
}).
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id").
Where("marketing_delivery_products.delivery_date IS NOT NULL")
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id")
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" || filters.MarketingType != "" {
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" {
db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id")
}
if filters.ProductId > 0 || filters.Search != "" || filters.MarketingType != "" {
if filters.ProductId > 0 || filters.Search != "" {
db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id")
}
@@ -178,29 +139,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId)
}
if filters.MarketingType != "" {
db = db.Joins("LEFT JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
Group("marketing_delivery_products.id")
switch filters.MarketingType {
case "ayam":
db = db.Where("flags.name IN (?)", []string{
string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer),
string(utils.FlagAyamAfkir), string(utils.FlagAyamCulling), string(utils.FlagAyamMati),
})
case "telur":
db = db.Where("flags.name IN (?)", []string{
string(utils.FlagTelur), string(utils.FlagTelurUtuh), string(utils.FlagTelurPecah),
string(utils.FlagTelurPutih), string(utils.FlagTelurRetak),
})
case "trading":
db = db.Where("flags.name IN (?)", []string{
string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia),
string(utils.FlagPakan), string(utils.FlagPreStarter), string(utils.FlagStarter), string(utils.FlagFinisher),
})
}
}
if filters.FilterBy != "" && (filters.StartDate != "" || filters.EndDate != "") {
if filters.FilterBy == "so_date" {
if filters.StartDate != "" {
@@ -52,7 +52,7 @@ func (s areaService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Ar
areas, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%")
return db.Where("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
@@ -51,7 +51,7 @@ func (s bankService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Ba
banks, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%")
return db.Where("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
@@ -3,13 +3,14 @@ package service
import (
"errors"
"fmt"
"strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -53,7 +54,7 @@ func (s customerService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
customers, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%")
return db.Where("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
@@ -53,7 +53,7 @@ func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
employeess, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
db = db.Where("employees.name ILIKE ?", "%"+params.Search+"%")
db = db.Where("LOWER(employees.name) LIKE LOWER(?)", "%"+params.Search+"%")
}
if params.KandangId != nil {
db = db.Joins("JOIN employee_kandangs ek ON ek.employee_id = employees.id").
@@ -55,7 +55,7 @@ func (s fcrService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Fcr
fcrs, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%")
return db.Where("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
@@ -52,7 +52,7 @@ func (s flockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.F
flocks, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%")
return db.Where("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
@@ -3,13 +3,14 @@ package service
import (
"errors"
"fmt"
"strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -54,7 +55,7 @@ func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
kandangs, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%")
return db.Where("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
}
if params.LocationId != 0 {
db = db.Where("location_id = ?", params.LocationId)
@@ -20,7 +20,7 @@ type Update struct {
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=500"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
LocationId int `query:"location_id" validate:"omitempty,number,gt=0"`
PicId int `query:"pic_id" validate:"omitempty,number,gt=0"`
@@ -52,7 +52,7 @@ func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
locations, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
db = db.Where("name ILIKE ?", "%"+params.Search+"%")
db = db.Where("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
}
if params.AreaId != 0 {
db = db.Where("area_id = ?", params.AreaId)
@@ -14,7 +14,7 @@ type Update struct {
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=500"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
AreaId int `query:"area_id" validate:"omitempty,number,gt=0"`
}
@@ -4,6 +4,7 @@ import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
@@ -22,7 +23,7 @@ type NonstockListDTO struct {
Name string `json:"name"`
Flags []string `json:"flags"`
Uom *uomDTO.UomRelationDTO `json:"uom"`
Suppliers []NonstockSupplierDTO `json:"suppliers"`
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@@ -32,14 +33,6 @@ type NonstockDetailDTO struct {
NonstockListDTO
}
type NonstockSupplierDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Alias string `json:"alias"`
Category string `json:"category"`
Price float64 `json:"price"`
}
// === Mapper Functions ===
func ToNonstockRelationDTO(e entity.Nonstock) NonstockRelationDTO {
@@ -106,27 +99,21 @@ func ToNonstockDetailDTO(e entity.Nonstock) NonstockDetailDTO {
}
}
func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []NonstockSupplierDTO {
func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []supplierDTO.SupplierRelationDTO {
if len(relations) == 0 {
return make([]NonstockSupplierDTO, 0)
return make([]supplierDTO.SupplierRelationDTO, 0)
}
result := make([]NonstockSupplierDTO, 0, len(relations))
result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations))
for _, relation := range relations {
if relation.Supplier.Id == 0 {
continue
}
result = append(result, NonstockSupplierDTO{
Id: relation.Supplier.Id,
Name: relation.Supplier.Name,
Alias: relation.Supplier.Alias,
Category: relation.Supplier.Category,
Price: relation.Price,
})
result = append(result, supplierDTO.ToSupplierRelationDTO(relation.Supplier))
}
if len(result) == 0 {
return make([]NonstockSupplierDTO, 0)
return make([]supplierDTO.SupplierRelationDTO, 0)
}
return result
@@ -12,7 +12,7 @@ import (
type NonstockRepository interface {
repository.BaseRepository[entity.Nonstock]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, suppliers []entity.NonstockSupplier) error
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, supplierIDs []uint) error
UomExists(ctx context.Context, uomID uint) (bool, error)
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
SyncFlags(ctx context.Context, tx *gorm.DB, nonstockID uint, flags []string) error
@@ -40,13 +40,13 @@ func (r *NonstockRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, e
return repository.Exists[entity.Nonstock](ctx, r.DB(), id)
}
func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, suppliers []entity.NonstockSupplier) error {
func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, supplierIDs []uint) error {
db := tx
if db == nil {
db = r.DB()
}
if suppliers == nil {
if supplierIDs == nil {
return db.WithContext(ctx).
Where("nonstock_id = ?", nonstockID).
Delete(&entity.NonstockSupplier{}).
@@ -61,31 +61,18 @@ func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm
return err
}
existingMap := make(map[uint]entity.NonstockSupplier, len(existing))
existingMap := make(map[uint]struct{}, len(existing))
for _, rel := range existing {
existingMap[rel.SupplierId] = rel
existingMap[rel.SupplierId] = struct{}{}
}
incomingMap := make(map[uint]struct{}, len(suppliers))
for _, rel := range suppliers {
incomingMap[rel.SupplierId] = struct{}{}
if existingRel, exists := existingMap[rel.SupplierId]; exists {
if existingRel.Price != rel.Price {
if err := db.WithContext(ctx).
Model(&entity.NonstockSupplier{}).
Where("nonstock_id = ? AND supplier_id = ?", nonstockID, rel.SupplierId).
Update("price", rel.Price).
Error; err != nil {
return err
}
}
incomingMap := make(map[uint]struct{}, len(supplierIDs))
for _, id := range supplierIDs {
incomingMap[id] = struct{}{}
if _, exists := existingMap[id]; exists {
continue
}
record := entity.NonstockSupplier{
NonstockId: nonstockID,
SupplierId: rel.SupplierId,
Price: rel.Price,
}
record := entity.NonstockSupplier{NonstockId: nonstockID, SupplierId: id}
if err := db.WithContext(ctx).Create(&record).Error; err != nil {
return err
}
@@ -68,7 +68,7 @@ func (s nonstockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%")
return db.Where("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
@@ -111,25 +111,8 @@ func (s *nonstockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
return nil, err
}
var (
supplierLinks []entity.NonstockSupplier
supplierIDs []uint
)
if len(req.Suppliers) > 0 {
seen := make(map[uint]struct{}, len(req.Suppliers))
supplierLinks = make([]entity.NonstockSupplier, 0, len(req.Suppliers))
supplierIDs = make([]uint, 0, len(req.Suppliers))
for _, supplier := range req.Suppliers {
if _, exists := seen[supplier.SupplierID]; exists {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplier.SupplierID))
}
seen[supplier.SupplierID] = struct{}{}
supplierIDs = append(supplierIDs, supplier.SupplierID)
supplierLinks = append(supplierLinks, entity.NonstockSupplier{
SupplierId: supplier.SupplierID,
Price: supplier.Price,
})
}
supplierIDs := utils.UniqueUintSlice(req.SupplierIDs)
if len(supplierIDs) > 0 {
supplierList, supplierErr := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
if supplierErr != nil {
s.Log.Errorf("Failed to validate suppliers: %+v", supplierErr)
@@ -172,7 +155,7 @@ func (s *nonstockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
return err
}
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierLinks)
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierIDs)
})
if err != nil {
@@ -210,27 +193,15 @@ func (s nonstockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint
updateBody["uom_id"] = *req.UomID
}
var supplierLinks []entity.NonstockSupplier
var supplierIDs []uint
var supplierUpdate bool
if req.Suppliers != nil {
if req.SupplierIDs != nil {
supplierUpdate = true
if len(*req.Suppliers) > 0 {
seen := make(map[uint]struct{}, len(*req.Suppliers))
supplierLinks = make([]entity.NonstockSupplier, 0, len(*req.Suppliers))
supplierIDs := make([]uint, 0, len(*req.Suppliers))
for _, supplier := range *req.Suppliers {
if _, exists := seen[supplier.SupplierID]; exists {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplier.SupplierID))
}
seen[supplier.SupplierID] = struct{}{}
supplierIDs = append(supplierIDs, supplier.SupplierID)
supplierLinks = append(supplierLinks, entity.NonstockSupplier{
SupplierId: supplier.SupplierID,
Price: supplier.Price,
})
}
supplierList, supplierErr := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
supplierIDs = utils.UniqueUintSlice(*req.SupplierIDs)
if len(supplierIDs) > 0 {
var supplierList []entity.Supplier
var supplierErr error
supplierList, supplierErr = s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
if supplierErr != nil {
s.Log.Errorf("Failed to validate suppliers: %+v", supplierErr)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers")
@@ -282,7 +253,11 @@ func (s nonstockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint
}
if supplierUpdate {
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, supplierLinks); err != nil {
var ids []uint
if len(supplierIDs) > 0 {
ids = supplierIDs
}
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, ids); err != nil {
return err
}
}
@@ -1,21 +1,16 @@
package validation
type SupplierPrice struct {
SupplierID uint `json:"supplier_id" validate:"required,gt=0"`
Price float64 `json:"price" validate:"required,gte=0"`
}
type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"`
UomID uint `json:"uom_id" validate:"required,gt=0"`
Suppliers []SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
SupplierIDs []uint `json:"supplier_ids" validate:"dive,gt=0"`
Flags []string `json:"flags" validate:"dive,max=50"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=50"`
UomID *uint `json:"uom_id,omitempty" validate:"omitempty,gt=0"`
Suppliers *[]SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
SupplierIDs *[]uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive,max=50"`
}
@@ -56,7 +56,7 @@ func (s phaseActivityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]
phaseActivitys, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
db = db.Where("name ILIKE ?", "%"+params.Search+"%")
db = db.Where("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
}
if params.PhaseIDs != "" {
ids := parseIDs(params.PhaseIDs)
@@ -15,13 +15,12 @@ type PhasesRelationDTO struct {
}
type PhasesListDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
IsActive bool `json:"is_active"`
ActivityCount int `json:"activity_count"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
Id uint `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
IsActive bool `json:"is_active"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
}
type PhasesDetailDTO struct {
@@ -45,13 +44,12 @@ func ToPhasesListDTO(e entity.Phases) PhasesListDTO {
// }
return PhasesListDTO{
Id: e.Id,
Name: e.Name,
Category: e.Category,
IsActive: e.IsActive,
ActivityCount: e.ActivityCount,
CreatedAt: e.CreatedAt,
CreatedUser: createdUser,
Id: e.Id,
Name: e.Name,
Category: e.Category,
IsActive: e.IsActive,
CreatedAt: e.CreatedAt,
CreatedUser: createdUser,
}
}
@@ -51,7 +51,7 @@ func (s phasesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.
phasess, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%")
return db.Where("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
}
if params.Category != nil {
db = db.Where("category = ?", *params.Category)
@@ -63,40 +63,6 @@ func (s phasesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.
s.Log.Errorf("Failed to get phasess: %+v", err)
return nil, 0, err
}
if len(phasess) > 0 {
ids := make([]uint, 0, len(phasess))
for _, phase := range phasess {
ids = append(ids, phase.Id)
}
type activityCountRow struct {
PhaseID uint
Count int64
}
var rows []activityCountRow
if err := s.Repository.DB().WithContext(c.Context()).
Table("phase_activities").
Select("phase_id, COUNT(*) AS count").
Where("phase_id IN ? AND deleted_at IS NULL", ids).
Group("phase_id").
Scan(&rows).Error; err != nil {
s.Log.Errorf("Failed to count phase activities: %+v", err)
return nil, 0, err
}
countMap := make(map[uint]int64, len(rows))
for _, row := range rows {
countMap[row.PhaseID] = row.Count
}
for i := range phasess {
if count, ok := countMap[phasess[i].Id]; ok {
phasess[i].ActivityCount = int(count)
}
}
}
return phasess, total, nil
}
@@ -52,7 +52,7 @@ func (s productCategoryService) GetAll(c *fiber.Ctx, params *validation.Query) (
productCategories, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%")
return db.Where("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
@@ -1,10 +1,8 @@
package service
import (
"context"
"errors"
"fmt"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
@@ -24,8 +22,6 @@ type ProductionStandardService interface {
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProductionStandard, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProductionStandard, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
EnsureWeekStart(ctx context.Context, standardID uint, category string) error
EnsureWeekAvailable(ctx context.Context, standardID uint, category string, day int) error
}
type productionStandardService struct {
@@ -67,7 +63,7 @@ func (s productionStandardService) GetAll(c *fiber.Ctx, params *validation.Query
productionStandards, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%")
return db.Where("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
}
if params.ProjectCategory != "" {
return db.Where("project_category = ?", params.ProjectCategory)
@@ -303,80 +299,3 @@ func (s productionStandardService) DeleteOne(c *fiber.Ctx, id uint) error {
}
return nil
}
func (s productionStandardService) EnsureWeekStart(ctx context.Context, standardID uint, category string) error {
if standardID == 0 || strings.TrimSpace(category) == "" {
return nil
}
switch strings.ToUpper(category) {
case string(utils.ProjectFlockCategoryLaying):
details, err := s.ProductionStandardDetailRepo.GetByProductionStandardID(ctx, standardID)
if err != nil {
return err
}
startWeek := 0
if len(details) > 0 {
startWeek = details[0].Week
}
if startWeek != 18 {
return fiber.NewError(fiber.StatusBadRequest, "Week tidak sesuai dengan standart kategori project flock")
}
case string(utils.ProjectFlockCategoryGrowing):
details, err := s.StandardGrowthDetailRepo.GetByProductionStandardID(ctx, standardID)
if err != nil {
return err
}
startWeek := 0
if len(details) > 0 {
startWeek = details[0].Week
}
if startWeek != 1 {
return fiber.NewError(fiber.StatusBadRequest, "Week tidak sesuai dengan standart kategori project flock")
}
}
return nil
}
func (s productionStandardService) EnsureWeekAvailable(ctx context.Context, standardID uint, category string, day int) error {
if standardID == 0 || day <= 0 {
return nil
}
upperCategory := strings.ToUpper(category)
weekBase := 1
if upperCategory == string(utils.ProjectFlockCategoryLaying) {
weekBase = 18
}
week := ((day - 1) / 7) + weekBase
if week <= 0 {
return nil
}
if upperCategory == string(utils.ProjectFlockCategoryLaying) {
detail, err := s.ProductionStandardDetailRepo.GetByStandardIDAndWeek(ctx, standardID, week)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Standart production tidak tersedia untuk week %d", week))
}
return err
}
if detail == nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Standart production tidak tersedia untuk week %d", week))
}
}
growthDetail, err := s.StandardGrowthDetailRepo.GetByStandardIDAndWeek(ctx, standardID, week)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Standart production tidak tersedia untuk week %d", week))
}
return err
}
if growthDetail == nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Standart production tidak tersedia untuk week %d", week))
}
return nil
}
@@ -5,6 +5,7 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
@@ -19,7 +20,7 @@ type ProductRelationDTO struct {
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flags *[]string `json:"flags,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
Suppliers []ProductSupplierDTO `json:"suppliers"`
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
}
type ProductListDTO struct {
@@ -34,7 +35,7 @@ type ProductListDTO struct {
Flags []string `json:"flags"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
Suppliers []ProductSupplierDTO `json:"suppliers"`
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@@ -44,14 +45,6 @@ type ProductDetailDTO struct {
ProductListDTO
}
type ProductSupplierDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Alias string `json:"alias"`
Category string `json:"category"`
Price float64 `json:"price"`
}
// === Mapper Functions ===
func ToProductRelationDTO(e entity.Product) ProductRelationDTO {
@@ -141,27 +134,21 @@ func ToProductDetailDTO(e entity.Product) ProductDetailDTO {
}
}
func toProductSupplierDTOs(relations []entity.ProductSupplier) []ProductSupplierDTO {
func toProductSupplierDTOs(relations []entity.ProductSupplier) []supplierDTO.SupplierRelationDTO {
if len(relations) == 0 {
return make([]ProductSupplierDTO, 0)
return make([]supplierDTO.SupplierRelationDTO, 0)
}
result := make([]ProductSupplierDTO, 0, len(relations))
result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations))
for _, relation := range relations {
if relation.Supplier.Id == 0 {
continue
}
result = append(result, ProductSupplierDTO{
Id: relation.Supplier.Id,
Name: relation.Supplier.Name,
Alias: relation.Supplier.Alias,
Category: relation.Supplier.Category,
Price: relation.Price,
})
result = append(result, supplierDTO.ToSupplierRelationDTO(relation.Supplier))
}
if len(result) == 0 {
return make([]ProductSupplierDTO, 0)
return make([]supplierDTO.SupplierRelationDTO, 0)
}
return result
@@ -17,7 +17,7 @@ type ProductRepository interface {
CategoryExists(ctx context.Context, categoryID uint) (bool, error)
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
IsLinkedToSupplier(ctx context.Context, productID, supplierID uint) (bool, error)
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, suppliers []entity.ProductSupplier) error
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error
SyncFlags(ctx context.Context, tx *gorm.DB, productID uint, flags []string) error
DeleteFlags(ctx context.Context, tx *gorm.DB, productID uint) error
GetFlags(ctx context.Context, productID uint) ([]entity.Flag, error)
@@ -102,13 +102,13 @@ func (r *ProductRepositoryImpl) IsLinkedToSupplier(ctx context.Context, productI
return count > 0, nil
}
func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, suppliers []entity.ProductSupplier) error {
func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIds []uint) error {
db := tx
if db == nil {
db = r.DB()
}
if suppliers == nil {
if supplierIds == nil {
return db.WithContext(ctx).
Where("product_id = ?", productID).
Delete(&entity.ProductSupplier{}).
@@ -123,31 +123,18 @@ func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.
return err
}
existingMap := make(map[uint]entity.ProductSupplier, len(existing))
existingMap := make(map[uint]struct{}, len(existing))
for _, rel := range existing {
existingMap[rel.SupplierId] = rel
existingMap[rel.SupplierId] = struct{}{}
}
incomingMap := make(map[uint]struct{}, len(suppliers))
for _, rel := range suppliers {
incomingMap[rel.SupplierId] = struct{}{}
if existingRel, exists := existingMap[rel.SupplierId]; exists {
if existingRel.Price != rel.Price {
if err := db.WithContext(ctx).
Model(&entity.ProductSupplier{}).
Where("product_id = ? AND supplier_id = ?", productID, rel.SupplierId).
Update("price", rel.Price).
Error; err != nil {
return err
}
}
incomingMap := make(map[uint]struct{}, len(supplierIds))
for _, id := range supplierIds {
incomingMap[id] = struct{}{}
if _, exists := existingMap[id]; exists {
continue
}
record := entity.ProductSupplier{
ProductId: productID,
SupplierId: rel.SupplierId,
Price: rel.Price,
}
record := entity.ProductSupplier{ProductId: productID, SupplierId: id}
if err := db.WithContext(ctx).Create(&record).Error; err != nil {
return err
}
@@ -72,7 +72,7 @@ func (s productService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
db = s.withRelations(db)
db = db.Where("is_visible = ?", true)
if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%")
return db.Where("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
}
if params.ProductCategoryID != 0 {
return db.Where("product_category_id = ?", params.ProductCategoryID)
@@ -138,25 +138,9 @@ func (s *productService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, err
}
var (
supplierLinks []entity.ProductSupplier
supplierIDs []uint
)
if len(req.Suppliers) > 0 {
seen := make(map[uint]struct{}, len(req.Suppliers))
supplierLinks = make([]entity.ProductSupplier, 0, len(req.Suppliers))
supplierIDs = make([]uint, 0, len(req.Suppliers))
for _, supplier := range req.Suppliers {
if _, exists := seen[supplier.SupplierID]; exists {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplier.SupplierID))
}
seen[supplier.SupplierID] = struct{}{}
supplierIDs = append(supplierIDs, supplier.SupplierID)
supplierLinks = append(supplierLinks, entity.ProductSupplier{
SupplierId: supplier.SupplierID,
Price: supplier.Price,
})
}
supplierIDs := utils.UniqueUintSlice(req.SupplierIDs)
var err error
if len(supplierIDs) > 0 {
suppliers, err := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
if err != nil {
s.Log.Errorf("Failed to validate suppliers: %+v", err)
@@ -192,11 +176,10 @@ func (s *productService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
SellingPrice: req.SellingPrice,
Tax: req.Tax,
ExpiryPeriod: req.ExpiryPeriod,
IsVisible: true,
CreatedBy: 1,
}
err := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
err = s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if err := repoTx.CreateOne(ctx, createBody, nil); err != nil {
@@ -207,7 +190,7 @@ func (s *productService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return err
}
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierLinks)
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierIDs)
})
if err != nil {
@@ -292,27 +275,15 @@ func (s productService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
ctx := c.Context()
var supplierLinks []entity.ProductSupplier
var suppliers []entity.Supplier
var supplierIDs []uint
var supplierUpdate bool
if req.Suppliers != nil {
if req.SupplierIDs != nil {
supplierUpdate = true
if len(*req.Suppliers) > 0 {
seen := make(map[uint]struct{}, len(*req.Suppliers))
supplierLinks = make([]entity.ProductSupplier, 0, len(*req.Suppliers))
supplierIDs := make([]uint, 0, len(*req.Suppliers))
for _, supplier := range *req.Suppliers {
if _, exists := seen[supplier.SupplierID]; exists {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplier.SupplierID))
}
seen[supplier.SupplierID] = struct{}{}
supplierIDs = append(supplierIDs, supplier.SupplierID)
supplierLinks = append(supplierLinks, entity.ProductSupplier{
SupplierId: supplier.SupplierID,
Price: supplier.Price,
})
}
suppliers, err := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
supplierIDs = utils.UniqueUintSlice(*req.SupplierIDs)
if len(supplierIDs) > 0 {
var err error
suppliers, err = s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
if err != nil {
s.Log.Errorf("Failed to validate suppliers: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers")
@@ -364,7 +335,11 @@ func (s productService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
}
if supplierUpdate {
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, supplierLinks); err != nil {
var ids []uint
if len(supplierIDs) > 0 {
ids = supplierIDs
}
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, ids); err != nil {
return err
}
}
@@ -1,10 +1,5 @@
package validation
type SupplierPrice struct {
SupplierID uint `json:"supplier_id" validate:"required,gt=0"`
Price float64 `json:"price" validate:"required,gte=0"`
}
type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"`
Brand string `json:"brand" validate:"required_strict,min=2,max=50"`
@@ -15,7 +10,7 @@ type Create struct {
SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"`
Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"`
Suppliers []SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
SupplierIDs []uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
Flags []string `json:"flags,omitempty" validate:"omitempty,dive"`
}
@@ -29,7 +24,7 @@ type Update struct {
SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"`
Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"`
Suppliers *[]SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
SupplierIDs *[]uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive"`
}
@@ -10,7 +10,6 @@ import (
type SupplierNonstockDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flags []string `json:"flags"`
}
@@ -43,7 +42,6 @@ func toSupplierNonstockDTOs(relations []entity.NonstockSupplier) []SupplierNonst
result = append(result, SupplierNonstockDTO{
Id: Nonstock.Id,
Name: Nonstock.Name,
Price: relation.Price,
Uom: uomRef,
Flags: flags,
})
@@ -8,13 +8,12 @@ import (
// === DTO Structs ===
type SupplierProductDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
ProductPrice float64 `gorm:"type:numeric(15,3);not null"`
SellingPrice *float64 `gorm:"type:numeric(15,3)"`
SupplierPrice float64 `json:"supplier_price"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flags []string `json:"flags"`
Id uint `json:"id"`
Name string `json:"name"`
ProductPrice float64 `gorm:"type:numeric(15,3);not null"`
SellingPrice *float64 `gorm:"type:numeric(15,3)"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flags []string `json:"flags"`
}
// === Mapper Functions ===
@@ -43,13 +42,12 @@ func toSupplierProductDTOs(relations []entity.ProductSupplier) []SupplierProduct
}
result = append(result, SupplierProductDTO{
Id: product.Id,
Name: product.Name,
ProductPrice: product.ProductPrice,
SellingPrice: product.SellingPrice,
SupplierPrice: relation.Price,
Uom: uomRef,
Flags: flags,
Id: product.Id,
Name: product.Name,
ProductPrice: product.ProductPrice,
SellingPrice: product.SellingPrice,
Uom: uomRef,
Flags: flags,
})
}
return result
@@ -65,11 +65,11 @@ func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
suppliers, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%")
return db.Where("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
}
if params.Category != "" {
db = db.Where("category ILIKE ?", "%"+params.Category+"%")
db = db.Where("LOWER(category) LIKE LOWER(?)", "%"+params.Category+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
+11 -5
View File
@@ -15,9 +15,15 @@ func UomRoutes(v1 fiber.Router, u user.UserService, s uom.UomService) {
route := v1.Group("/uoms")
route.Use(m.Auth(u))
route.Get("/", m.RequirePermissions(m.P_UomsGetAll), ctrl.GetAll)
route.Post("/", m.RequirePermissions(m.P_UomsCreateOne), ctrl.CreateOne)
route.Get("/:id", m.RequirePermissions(m.P_UomsGetOne), ctrl.GetOne)
route.Patch("/:id", m.RequirePermissions(m.P_UomsUpdateOne), ctrl.UpdateOne)
route.Delete("/:id", m.RequirePermissions(m.P_UomsDeleteOne), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
route.Get("/",m.RequirePermissions(m.P_AreaGetAll), ctrl.GetAll)
route.Post("/",m.RequirePermissions(m.P_AreaCreateOne), ctrl.CreateOne)
route.Get("/:id",m.RequirePermissions(m.P_AreaGetOne), ctrl.GetOne)
route.Patch("/:id",m.RequirePermissions(m.P_AreaUpdateOne), ctrl.UpdateOne)
route.Delete("/:id",m.RequirePermissions(m.P_AreaDeleteOne), ctrl.DeleteOne)
}
@@ -51,7 +51,7 @@ func (s uomService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Uom
uoms, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%")
return db.Where("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
@@ -3,13 +3,14 @@ package service
import (
"errors"
"fmt"
"strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -53,7 +54,7 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
warehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
db = db.Where("warehouses.name ILIKE ?", "%"+params.Search+"%")
db = db.Where("LOWER(warehouses.name) LIKE LOWER(?)", "%"+params.Search+"%")
}
if params.AreaId != 0 {
db = db.Where("area_id = ?", params.AreaId)
@@ -14,7 +14,6 @@ import (
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
sChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services"
@@ -39,7 +38,6 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
projectflockpopulationrepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
productRepo := rProduct.NewProductRepository(db)
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
userRepo := rUser.NewUserRepository(db)
@@ -90,7 +88,6 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
kandangRepo,
warehouseRepo,
productWarehouseRepo,
productRepo,
projectFlockRepo,
projectflockkandangrepo,
projectflockpopulationrepo,
@@ -12,7 +12,6 @@ import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations"
@@ -45,7 +44,6 @@ type chickinService struct {
KandangRepo KandangRepo.KandangRepository
WarehouseRepo rWarehouse.WarehouseRepository
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
ProductRepo rProduct.ProductRepository
ProjectFlockRepo rProjectFlock.ProjectflockRepository
ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository
ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
@@ -54,7 +52,7 @@ type chickinService struct {
StockLogRepo rStockLogs.StockLogRepository
}
func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, productRepo rProduct.ProductRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate, fifoSvc commonSvc.FifoService) ChickinService {
func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate, fifoSvc commonSvc.FifoService) ChickinService {
return &chickinService{
Log: utils.Log,
Validate: validate,
@@ -62,7 +60,6 @@ func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo Kan
KandangRepo: kandangRepo,
WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo,
ProductRepo: productRepo,
ProjectFlockRepo: projectFlockRepo,
ProjectflockKandangRepo: projectflockkandangRepo,
ProjectflockPopulationRepo: projectflockpopulationRepo,
@@ -102,6 +99,7 @@ func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get chickins: %+v", err)
return nil, 0, err
}
return chickins, total, nil
@@ -349,6 +347,7 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found")
}
s.Log.Errorf("Failed to update chickin: %+v", err)
return nil, err
}
@@ -381,6 +380,7 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
warehouseDeltas := make(map[uint]float64)
warehouseDeltas[chickin.ProductWarehouseId] += currentUsageQty
if err := s.adjustProductWarehouseQuantities(c.Context(), s.Repository.DB(), warehouseDeltas); err != nil {
s.Log.Errorf("Failed to adjust product warehouses for deleted chickin %d: %+v", chickin.Id, err)
return err
}
}
@@ -449,7 +449,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
for _, approvableID := range approvableIDs {
if _, err := approvalSvc.CreateApproval(
@@ -480,55 +479,39 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
category := strings.ToUpper(strings.TrimSpace(kandangForApproval.ProjectFlock.Category))
var targetFlag utils.FlagType
if category == string(utils.ProjectFlockCategoryGrowing) {
targetFlag = utils.FlagPullet
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
}
pfkID := approvableID
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID, &pfkID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse")
}
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
}
} else if category == string(utils.ProjectFlockCategoryLaying) {
targetFlag = utils.FlagLayer
} else {
continue
}
for _, chickin := range chickins {
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(c.Context(), chickin.Id)
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to check population for chickin %d", chickin.Id))
}
if populationExists {
continue
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
}
sourcePW, err := s.ProductWarehouseRepo.GetByID(c.Context(), chickin.ProductWarehouseId, func(db *gorm.DB) *gorm.DB {
return db.Preload("Product.Flags")
})
pfkID := approvableID
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "LAYER", dbTransaction, actorID, &pfkID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get product warehouse for chickin %d", chickin.Id))
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create LAYER product warehouse")
}
if err := s.autoAddFlagToProduct(c.Context(), dbTransaction, sourcePW.Product.Id, targetFlag); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to auto-add flag to product %d", sourcePW.Product.Id))
}
population := &entity.ProjectFlockPopulation{
ProjectChickinId: chickin.Id,
ProductWarehouseId: sourcePW.Id,
TotalQty: 0,
TotalUsedQty: 0,
Notes: chickin.Notes,
CreatedBy: actorID,
}
if err := ProjectFlockPopulationRepotx.CreateOne(c.Context(), population, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to create population for chickin %d", chickin.Id))
}
if err := chickinRepoTx.PatchOne(c.Context(), chickin.Id, map[string]any{
"pending_usage_qty": 0,
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to reset pending usage qty for chickin %d", chickin.Id))
}
if err := s.ReplenishChickinStocks(c.Context(), dbTransaction, &chickin, sourcePW, population, actorID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock for chickin %d", chickin.Id))
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
}
}
}
@@ -551,6 +534,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
warehouseDeltas := make(map[uint]float64)
warehouseDeltas[chickin.ProductWarehouseId] += chickin.UsageQty
if err := s.adjustProductWarehouseQuantities(c.Context(), dbTransaction, warehouseDeltas); err != nil {
s.Log.Errorf("Failed to adjust product warehouses for rejected chickin %d: %+v", chickin.Id, err)
return err
}
@@ -584,35 +568,104 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
return updated, nil
}
// autoAddFlagToProduct adds target flag to product if not already present (idempotent)
func (s *chickinService) autoAddFlagToProduct(ctx context.Context, tx *gorm.DB, productID uint, targetFlag utils.FlagType) error {
if s.ProductRepo == nil {
return nil
}
func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint, projectFlockKandangId *uint) (*entity.ProductWarehouse, error) {
currentFlags, err := s.ProductRepo.GetFlags(ctx, productID)
if err != nil {
return fmt.Errorf("failed to get product flags: %w", err)
}
products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(ctx.Context(), categoryCode, warehouseId)
if err == nil && len(products) > 0 {
existingPW := &products[0]
hasTargetFlag := false
currentFlagNames := make([]string, 0, len(currentFlags))
for _, flag := range currentFlags {
currentFlagNames = append(currentFlagNames, flag.Name)
if flag.Name == string(targetFlag) {
hasTargetFlag = true
if existingPW.ProjectFlockKandangId == nil && projectFlockKandangId != nil {
existingPW.ProjectFlockKandangId = projectFlockKandangId
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).UpdateOne(ctx.Context(), existingPW.Id, existingPW, nil); err != nil {
return nil, fmt.Errorf("failed to update %s product warehouse with project_flock_kandang_id: %w", categoryCode, err)
}
}
return existingPW, nil
}
if hasTargetFlag {
return nil
product, err := s.ProductWarehouseRepo.GetFirstProductByFlag(ctx.Context(), categoryCode)
if err != nil {
return nil, fmt.Errorf("failed to get %s product: %w", categoryCode, err)
}
if product == nil {
return nil, fmt.Errorf("no %s product found in system", categoryCode)
}
newFlags := append(currentFlagNames, string(targetFlag))
if err := s.ProductRepo.SyncFlags(ctx, tx, productID, newFlags); err != nil {
return fmt.Errorf("failed to sync flags: %w", err)
newPW := &entity.ProductWarehouse{
ProductId: product.Id,
WarehouseId: warehouseId,
ProjectFlockKandangId: projectFlockKandangId,
Quantity: 0,
}
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPW, nil); err != nil {
return nil, fmt.Errorf("failed to create %s product warehouse: %w", categoryCode, err)
}
return newPW, nil
}
func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []entity.ProjectChickin, targetPW *entity.ProductWarehouse, dbTransaction *gorm.DB, actorID uint) error {
if targetPW == nil || targetPW.Id == 0 {
return fmt.Errorf("invalid target product warehouse")
}
ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
chickinRepoTx := s.Repository.WithTx(dbTransaction)
var totalQuantityAdded float64
for _, chickin := range chickins {
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id)
if err != nil {
return fmt.Errorf("failed to check population existence for chickin %d: %w", chickin.Id, err)
}
if populationExists {
s.Log.Infof("population already exists for chickin %d, skipping", chickin.Id)
continue
}
quantityToConvert := chickin.UsageQty
population := &entity.ProjectFlockPopulation{
ProjectChickinId: chickin.Id,
ProductWarehouseId: targetPW.Id,
TotalQty: 0, // Will be set by FIFO Replenish
TotalUsedQty: 0,
Notes: chickin.Notes,
CreatedBy: actorID,
}
if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil {
return err
}
// Reset PendingUsageQty to 0 since population has been created
if err := chickinRepoTx.PatchOne(ctx.Context(), chickin.Id, map[string]any{
"pending_usage_qty": 0,
}, nil); err != nil {
return fmt.Errorf("failed to reset pending usage qty for chickin %d: %w", chickin.Id, err)
}
// Replenish stock to target ProductWarehouse based on source flag
// StockableKey is PROJECT_CHICKIN but StockableID refers to Population ID
if err := s.ReplenishChickinStocks(ctx.Context(), dbTransaction, &chickin, targetPW, population, actorID); err != nil {
s.Log.Errorf("Failed to replenish stock for chickin %d: %+v", chickin.Id, err)
return err
}
totalQuantityAdded += quantityToConvert
}
// NOTE: ProductWarehouse target sudah ditambah melalui ReplenishChickinStocks
// yang dipanggil di atas untuk setiap chickin berdasarkan flag source:
// - DOC → replenish ke PULLET
// - PULLET → replenish ke LAYER
// - LAYER → tidak perlu replenish (sudah final)
// - DOC+PULLET+LAYER → replenish ke dirinya sendiri
return nil
}
@@ -621,6 +674,9 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
return nil
}
s.Log.Infof("ConsumeChickinStocks: chickin_id=%d, product_warehouse_id=%d, desired_qty=%.3f",
chickin.Id, chickin.ProductWarehouseId, desiredQty)
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: chickinUsableKey,
UsableID: chickin.Id,
@@ -630,9 +686,13 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
Tx: tx,
})
if err != nil {
s.Log.Errorf("Failed to consume FIFO stock for chickin %d: %+v", chickin.Id, err)
return err
}
s.Log.Infof("ConsumeChickinStocks result: usage_qty=%.3f, pending_qty=%.3f, allocated_allocations=%d",
result.UsageQuantity, result.PendingQuantity, len(result.AddedAllocations))
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
return err
}
@@ -646,7 +706,10 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
CreatedBy: actorID,
Notes: fmt.Sprintf("Chickin #%d", chickin.Id),
}
s.StockLogRepo.CreateOne(ctx, decreaseLog, nil)
if err := s.StockLogRepo.CreateOne(ctx, decreaseLog, nil); err != nil {
s.Log.Errorf("Failed to create stock log for chickin %d: %+v", chickin.Id, err)
}
}
return nil
@@ -657,17 +720,93 @@ func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB
return nil
}
_, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyProjectFlockPopulation,
StockableID: population.Id,
ProductWarehouseID: targetPW.Id,
Quantity: chickin.UsageQty,
Tx: tx,
sourcePW, err := s.ProductWarehouseRepo.GetByID(ctx, chickin.ProductWarehouseId, func(db *gorm.DB) *gorm.DB {
return db.Preload("Product.Flags")
})
if err != nil {
return err
}
if sourcePW == nil || sourcePW.Product.Id == 0 {
return fmt.Errorf("source product warehouse or product not found for chickin %d", chickin.Id)
}
sourceFlags := sourcePW.Product.Flags
if len(sourceFlags) == 0 {
s.Log.Warnf("Source product %d has no flags, skipping replenish for chickin %d", sourcePW.Product.Id, chickin.Id)
return nil
}
hasDoc := false
hasPullet := false
hasLayer := false
for _, flag := range sourceFlags {
flagName := utils.FlagType(flag.Name)
if flagName == utils.FlagDOC {
hasDoc = true
} else if flagName == utils.FlagPullet {
hasPullet = true
} else if flagName == utils.FlagLayer {
hasLayer = true
}
}
if hasDoc && hasPullet && hasLayer {
s.Log.Infof("Chickin %d has mixed flags (DOC+PULLET+LAYER), replenishing to source PW %d", chickin.Id, sourcePW.Id)
_, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyProjectFlockPopulation,
StockableID: population.Id,
ProductWarehouseID: sourcePW.Id,
Quantity: chickin.UsageQty,
Tx: tx,
})
if err != nil {
s.Log.Errorf("Failed to replenish stock to source PW for chickin %d: %+v", chickin.Id, err)
return err
}
return nil
}
// LAYER only - no replenish needed
if hasLayer && !hasDoc && !hasPullet {
s.Log.Infof("Chickin %d has LAYER flag only, skipping replenish", chickin.Id)
return nil
}
if hasDoc && !hasPullet && !hasLayer {
s.Log.Infof("Chickin %d has DOC flag, replenishing to PULLET PW %d", chickin.Id, targetPW.Id)
_, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyProjectFlockPopulation,
StockableID: population.Id,
ProductWarehouseID: targetPW.Id,
Quantity: chickin.UsageQty,
Tx: tx,
})
if err != nil {
s.Log.Errorf("Failed to replenish stock to PULLET PW for chickin %d: %+v", chickin.Id, err)
return err
}
return nil
}
if hasPullet && !hasDoc && !hasLayer {
s.Log.Infof("Chickin %d has PULLET flag, replenishing to LAYER PW %d", chickin.Id, targetPW.Id)
_, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyProjectFlockPopulation,
StockableID: population.Id,
ProductWarehouseID: targetPW.Id,
Quantity: chickin.UsageQty,
Tx: tx,
})
if err != nil {
s.Log.Errorf("Failed to replenish stock to LAYER PW for chickin %d: %+v", chickin.Id, err)
return err
}
return nil
}
// Other combinations (e.g., DOC + PULLET without LAYER) - skip for now
s.Log.Warnf("Chickin %d has unsupported flag combination, skipping replenish", chickin.Id)
return nil
}
@@ -686,6 +825,7 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
UsableID: chickin.Id,
Tx: tx,
}); err != nil {
s.Log.Errorf("Failed to release FIFO stock for chickin %d: %+v", chickin.Id, err)
return err
}
@@ -702,7 +842,9 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
CreatedBy: actorID,
Notes: fmt.Sprintf("Chickin #%d - Stock released", chickin.Id),
}
s.StockLogRepo.CreateOne(ctx, increaseLog, nil)
if err := s.StockLogRepo.CreateOne(ctx, increaseLog, nil); err != nil {
s.Log.Errorf("Failed to create stock log for released chickin %d: %+v", chickin.Id, err)
}
}
return nil
@@ -96,9 +96,9 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProjectFlockKandangI
var total float64
err := r.DB().WithContext(ctx).
Table("project_flock_populations").
Select("COALESCE(SUM(total_qty - total_used_qty), 0) AS available_qty").
Joins("JOIN product_warehouses pw ON project_flock_populations.product_warehouse_id = pw.id").
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
Select("COALESCE(SUM(total_qty), 0) AS total_qty").
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID).
Scan(&total).Error
if err != nil {
return 0, err
@@ -14,83 +14,44 @@ import (
// === DTO Structs ===
type RecordingProjectFlockDTO struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
FlockName string `json:"flock_name"`
ProjectFlockCategory string `json:"project_flock_category"`
Period int `json:"period"`
ProductionStandart *RecordingProductionStandardDTO `json:"production_standart,omitempty"`
Fcr *RecordingFcrDTO `json:"fcr,omitempty"`
TotalChickQty float64 `json:"total_chick_qty"`
}
type RecordingProductionStandardDTO struct {
Id uint `json:"id"`
Week int `json:"week"`
Name string `json:"name"`
HenDayStd float64 `json:"hen_day_std"`
HenHouseStd float64 `json:"hen_house_std"`
FeedIntakeStd float64 `json:"feed_intake_std"`
MaxDepletionStd float64 `json:"max_depletion_std"`
EggMassStd float64 `json:"egg_mass_std"`
EggWeightStd float64 `json:"egg_weight_std"`
}
type RecordingFcrDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
FcrStd float64 `json:"fcr_std"`
}
type RecordingAreaDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type RecordingLocationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Address string `json:"address"`
}
type RecordingWarehouseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Area *RecordingAreaDTO `json:"area,omitempty"`
Location *RecordingLocationDTO `json:"location,omitempty"`
}
type RecordingRelationDTO struct {
Id uint `json:"id"`
ProjectFlock RecordingProjectFlockDTO `json:"project_flock"`
RecordDatetime time.Time `json:"record_datetime"`
Day int `json:"day"`
TotalDepletionQty float64 `json:"total_depletion_qty"`
CumDepletionRate float64 `json:"cum_depletion_rate"`
CumIntake int `json:"cum_intake"`
FcrValue float64 `json:"fcr_value"`
HenDay float64 `json:"hen_day"`
HenHouse float64 `json:"hen_house"`
FeedIntake float64 `json:"feed_intake"`
EggMass float64 `json:"egg_mass"`
EggWeight float64 `json:"egg_weight"`
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
Id uint `json:"id"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
RecordDatetime time.Time `json:"record_datetime"`
Day int `json:"day"`
ProjectFlockCategory string `json:"project_flock_category"`
TotalDepletionQty float64 `json:"total_depletion_qty"`
CumDepletionRate float64 `json:"cum_depletion_rate"`
CumIntake int `json:"cum_intake"`
FcrValue float64 `json:"fcr_value"`
TotalChickQty float64 `json:"total_chick_qty"`
HenDay float64 `json:"hen_day"`
HenHouse float64 `json:"hen_house"`
FeedIntake float64 `json:"feed_intake"`
EggMass float64 `json:"egg_mass"`
EggWeight float64 `json:"egg_weight"`
StandardHenDay *float64 `json:"hen_day_std,omitempty"`
StandardHenHouse *float64 `json:"hen_house_std,omitempty"`
StandardFeedIntake *float64 `json:"feed_intake_std,omitempty"`
StandardMaxDepletion *float64 `json:"max_depletion_std,omitempty"`
StandardEggMass *float64 `json:"egg_mass_std,omitempty"`
StandardEggWeight *float64 `json:"egg_weight_std,omitempty"`
StandardFcr *float64 `json:"fcr_std,omitempty"`
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
}
type RecordingListDTO struct {
RecordingRelationDTO
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type RecordingDetailDTO struct {
RecordingListDTO
Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"`
ProductCategory string `json:"product_category"`
Depletions []RecordingDepletionDTO `json:"depletions"`
Stocks []RecordingStockDTO `json:"stocks"`
Eggs []RecordingEggDTO `json:"eggs"`
Depletions []RecordingDepletionDTO `json:"depletions"`
Stocks []RecordingStockDTO `json:"stocks"`
Eggs []RecordingEggDTO `json:"eggs"`
}
type RecordingDepletionDTO struct {
@@ -102,7 +63,7 @@ type RecordingDepletionDTO struct {
type RecordingStockDTO struct {
ProductWarehouseId uint `json:"product_warehouse_id"`
UsageAmount float64 `json:"usage_amount"`
PendingQty float64 `json:"pending_qty"`
PendingQty *float64 `json:"pending_qty,omitempty"`
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
}
@@ -114,10 +75,117 @@ type RecordingEggDTO struct {
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
}
type RecordingProductWarehouseDTO struct {
Id uint `json:"id"`
ProductId uint `json:"product_id"`
ProductName string `json:"product_name"`
WarehouseId uint `json:"warehouse_id"`
WarehouseName string `json:"warehouse_name"`
}
// === Mapper Functions ===
func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
var (
projectFlockCategory string
day int
totalDepletionQty float64
cumDepletionRate float64
cumIntake int
fcrValue float64
totalChickQty float64
henDay float64
henHouse float64
feedIntake float64
eggMass float64
eggWeight float64
)
if e.Day != nil {
day = *e.Day
}
if e.TotalDepletionQty != nil {
totalDepletionQty = *e.TotalDepletionQty
}
if e.CumDepletionRate != nil {
cumDepletionRate = *e.CumDepletionRate
}
if e.CumIntake != nil {
cumIntake = *e.CumIntake
}
if e.FcrValue != nil {
fcrValue = *e.FcrValue
}
if e.TotalChickQty != nil {
totalChickQty = *e.TotalChickQty
}
if e.HenDay != nil {
henDay = *e.HenDay
}
if e.HenHouse != nil {
henHouse = *e.HenHouse
}
if e.FeedIntake != nil {
feedIntake = *e.FeedIntake
}
if e.EggMass != nil {
eggMass = *e.EggMass
}
if e.EggWeight != nil {
eggWeight = *e.EggWeight
}
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 {
category := e.ProjectFlockKandang.ProjectFlock.Category
projectFlockCategory = category
}
latestApproval := defaultRecordingLatestApproval(e)
if e.LatestApproval != nil {
snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval)
latestApproval = snapshot
}
return RecordingRelationDTO{
Id: e.Id,
ProjectFlockKandangId: e.ProjectFlockKandangId,
RecordDatetime: e.RecordDatetime,
Day: day,
ProjectFlockCategory: projectFlockCategory,
TotalDepletionQty: totalDepletionQty,
CumDepletionRate: cumDepletionRate,
CumIntake: cumIntake,
FcrValue: fcrValue,
TotalChickQty: totalChickQty,
HenDay: henDay,
HenHouse: henHouse,
FeedIntake: feedIntake,
EggMass: eggMass,
EggWeight: eggWeight,
StandardHenDay: e.StandardHenDay,
StandardHenHouse: e.StandardHenHouse,
StandardFeedIntake: e.StandardFeedIntake,
StandardMaxDepletion: e.StandardMaxDepletion,
StandardEggMass: e.StandardEggMass,
StandardEggWeight: e.StandardEggWeight,
StandardFcr: e.StandardFcr,
Approval: latestApproval,
}
}
func ToRecordingListDTO(e entity.Recording) RecordingListDTO {
return toRecordingListDTO(e)
var createdUser *userDTO.UserRelationDTO
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(*e.CreatedUser)
createdUser = &mapped
}
return RecordingListDTO{
RecordingRelationDTO: ToRecordingRelationDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO {
@@ -129,15 +197,20 @@ func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO {
}
func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
listDTO := toRecordingListDTO(e)
listDTO := ToRecordingListDTO(e)
var eggs []RecordingEggDTO
if strings.EqualFold(listDTO.ProjectFlockCategory, string(utils.ProjectFlockCategoryLaying)) {
eggs = ToRecordingEggDTOs(e.Eggs)
} else if len(e.Eggs) > 0 {
eggs = ToRecordingEggDTOs(e.Eggs)
}
return RecordingDetailDTO{
RecordingListDTO: listDTO,
Warehouse: recordingWarehouseDTO(e),
ProductCategory: recordingProductCategory(e),
Depletions: ToRecordingDepletionDTOs(e.Depletions),
Stocks: ToRecordingStockDTOs(e.Stocks),
Eggs: ToRecordingEggDTOs(e.Eggs),
Depletions: ToRecordingDepletionDTOs(e.Depletions),
Stocks: ToRecordingStockDTOs(e.Stocks),
Eggs: eggs,
}
}
@@ -160,15 +233,11 @@ func ToRecordingStockDTOs(stocks []entity.RecordingStock) []RecordingStockDTO {
if s.UsageQty != nil {
usageAmount = *s.UsageQty
}
var pendingQty float64
if s.PendingQty != nil {
pendingQty = *s.PendingQty
}
result[i] = RecordingStockDTO{
ProductWarehouseId: s.ProductWarehouseId,
UsageAmount: usageAmount,
PendingQty: pendingQty,
PendingQty: s.PendingQty,
ProductWarehouse: mapProductWarehouseDTO(&s.ProductWarehouse),
}
}
@@ -189,184 +258,6 @@ func ToRecordingEggDTOs(eggs []entity.RecordingEgg) []RecordingEggDTO {
return result
}
func toRecordingListDTO(e entity.Recording) RecordingListDTO {
relation := toRecordingRelationDTO(e)
var createdUser *userDTO.UserRelationDTO
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(*e.CreatedUser)
createdUser = &mapped
}
return RecordingListDTO{
RecordingRelationDTO: relation,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func toRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
latestApproval := defaultRecordingLatestApproval(e)
if e.LatestApproval != nil {
snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval)
latestApproval = snapshot
}
return RecordingRelationDTO{
Id: e.Id,
ProjectFlock: toRecordingProjectFlockDTO(e),
RecordDatetime: e.RecordDatetime,
Day: intValue(e.Day),
TotalDepletionQty: floatValue(e.TotalDepletionQty),
CumDepletionRate: floatValue(e.CumDepletionRate),
CumIntake: intValue(e.CumIntake),
FcrValue: floatValue(e.FcrValue),
HenDay: floatValue(e.HenDay),
HenHouse: floatValue(e.HenHouse),
FeedIntake: floatValue(e.FeedIntake),
EggMass: floatValue(e.EggMass),
EggWeight: floatValue(e.EggWeight),
Approval: latestApproval,
}
}
func toRecordingProjectFlockDTO(e entity.Recording) RecordingProjectFlockDTO {
result := RecordingProjectFlockDTO{
ProjectFlockKandangId: e.ProjectFlockKandangId,
}
pfk := e.ProjectFlockKandang
if pfk == nil {
return result
}
if pfk.ProjectFlock.Id != 0 {
result.FlockName = pfk.ProjectFlock.FlockName
if pfk.ProjectFlock.Category != "" {
result.ProjectFlockCategory = strings.ToUpper(pfk.ProjectFlock.Category)
}
}
result.Period = pfk.Period
if pfk.ProjectFlock.ProductionStandard.Id != 0 {
result.ProductionStandart = &RecordingProductionStandardDTO{
Id: pfk.ProjectFlock.ProductionStandard.Id,
Week: recordingWeekValue(e),
Name: pfk.ProjectFlock.ProductionStandard.Name,
HenDayStd: floatValue(e.StandardHenDay),
HenHouseStd: floatValue(e.StandardHenHouse),
FeedIntakeStd: floatValue(e.StandardFeedIntake),
MaxDepletionStd: floatValue(e.StandardMaxDepletion),
EggMassStd: floatValue(e.StandardEggMass),
EggWeightStd: floatValue(e.StandardEggWeight),
}
}
if pfk.ProjectFlock.Fcr.Id != 0 || e.StandardFcr != nil {
result.Fcr = &RecordingFcrDTO{
Id: pfk.ProjectFlock.Fcr.Id,
Name: pfk.ProjectFlock.Fcr.Name,
FcrStd: floatValue(e.StandardFcr),
}
}
result.TotalChickQty = floatValue(e.TotalChickQty)
return result
}
func recordingWeekValue(e entity.Recording) int {
day := intValue(e.Day)
if day <= 0 {
return 0
}
weekBase := 1
if isLayingRecording(e) {
weekBase = 18
}
return ((day - 1) / 7) + weekBase
}
func isLayingRecording(e entity.Recording) bool {
if e.ProjectFlockKandang == nil {
return false
}
return strings.EqualFold(e.ProjectFlockKandang.ProjectFlock.Category, string(utils.ProjectFlockCategoryLaying))
}
func recordingProductCategory(e entity.Recording) string {
if e.ProjectFlockKandang == nil {
return ""
}
project := e.ProjectFlockKandang.ProjectFlock
if project.Id == 0 {
return ""
}
if project.ProductionStandard.Id != 0 && project.ProductionStandard.ProjectCategory != "" {
return strings.ToUpper(project.ProductionStandard.ProjectCategory)
}
if project.Category != "" {
return strings.ToUpper(project.Category)
}
return ""
}
func recordingWarehouseDTO(e entity.Recording) *RecordingWarehouseDTO {
pw := primaryProductWarehouse(e)
if pw == nil || pw.Warehouse.Id == 0 {
return nil
}
return mapWarehouseDTO(&pw.Warehouse)
}
func primaryProductWarehouse(e entity.Recording) *entity.ProductWarehouse {
if len(e.Stocks) > 0 {
pw := e.Stocks[0].ProductWarehouse
if pw.Id != 0 {
return &pw
}
}
if len(e.Depletions) > 0 {
pw := e.Depletions[0].ProductWarehouse
if pw.Id != 0 {
return &pw
}
}
if len(e.Eggs) > 0 {
pw := e.Eggs[0].ProductWarehouse
if pw.Id != 0 {
return &pw
}
}
return nil
}
func mapWarehouseDTO(wh *entity.Warehouse) *RecordingWarehouseDTO {
if wh == nil || wh.Id == 0 {
return nil
}
dto := &RecordingWarehouseDTO{
Id: wh.Id,
Name: wh.Name,
}
if wh.Area.Id != 0 {
dto.Area = &RecordingAreaDTO{
Id: wh.Area.Id,
Name: wh.Area.Name,
}
}
if wh.Location != nil && wh.Location.Id != 0 {
dto.Location = &RecordingLocationDTO{
Id: wh.Location.Id,
Name: wh.Location.Name,
Address: wh.Location.Address,
}
}
return dto
}
func mapProductWarehouseDTO(pw *entity.ProductWarehouse) productWarehouseDTO.ProductWarehouseDTO {
if pw == nil {
return productWarehouseDTO.ProductWarehouseDTO{}
@@ -380,20 +271,6 @@ func mapProductWarehouseDTO(pw *entity.ProductWarehouse) productWarehouseDTO.Pro
return *mapped
}
func floatValue(value *float64) float64 {
if value == nil {
return 0
}
return *value
}
func intValue(value *int) int {
if value == nil {
return 0
}
return *value
}
func defaultRecordingLatestApproval(e entity.Recording) approvalDTO.ApprovalRelationDTO {
result := approvalDTO.ApprovalRelationDTO{}
@@ -11,8 +11,6 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
sProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/services"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
@@ -31,16 +29,6 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db)
productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
standardGrowthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
productionStandardService := sProductionStandard.NewProductionStandardService(
productionStandardRepo,
productionStandardDetailRepo,
standardGrowthDetailRepo,
validate,
)
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
if err := fifoService.RegisterUsable(fifo.UsableConfig{
@@ -75,7 +63,6 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
approvalRepo,
approvalService,
fifoService,
productionStandardService,
validate,
)
userService := sUser.NewUserService(userRepo, validate)
@@ -48,30 +48,12 @@ type RecordingRepository interface {
GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error)
GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error)
GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeightKg float64, err error)
GetAverageTargetMetricsByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, includeTargets bool) (RecordingTargetAverages, error)
}
type RecordingRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Recording]
}
type RecordingTargetAverages struct {
HenDayAvg float64
HenDayCount int64
HenHouseAvg float64
HenHouseCount int64
EggWeightAvg float64
EggWeightCount int64
EggMassAvg float64
EggMassCount int64
FeedIntakeAvg float64
FeedIntakeCount int64
FcrAvg float64
FcrCount int64
CumDepletionRateAvg float64
CumDepletionRateCount int64
}
func NewRecordingRepository(db *gorm.DB) RecordingRepository {
return &RecordingRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Recording](db),
@@ -82,28 +64,19 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("ProjectFlockKandang").
Preload("ProjectFlockKandang.Kandang").
Preload("ProjectFlockKandang.ProjectFlock").
Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard").
Preload("ProjectFlockKandang.ProjectFlock.Fcr").
Preload("Depletions").
Preload("Depletions.ProductWarehouse").
Preload("Depletions.ProductWarehouse.Product").
Preload("Depletions.ProductWarehouse.Warehouse").
Preload("Depletions.ProductWarehouse.Warehouse.Area").
Preload("Depletions.ProductWarehouse.Warehouse.Location").
Preload("Stocks").
Preload("Stocks.ProductWarehouse").
Preload("Stocks.ProductWarehouse.Product").
Preload("Stocks.ProductWarehouse.Warehouse").
Preload("Stocks.ProductWarehouse.Warehouse.Area").
Preload("Stocks.ProductWarehouse.Warehouse.Location").
Preload("Eggs").
Preload("Eggs.ProductWarehouse").
Preload("Eggs.ProductWarehouse.Product").
Preload("Eggs.ProductWarehouse.Warehouse").
Preload("Eggs.ProductWarehouse.Warehouse.Area").
Preload("Eggs.ProductWarehouse.Warehouse.Location")
Preload("Eggs.ProductWarehouse.Warehouse")
}
func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) {
@@ -460,67 +433,6 @@ func (r *RecordingRepositoryImpl) GetTotalEggProductionWeightByProjectFlockID(ct
return result, err
}
func (r *RecordingRepositoryImpl) GetAverageTargetMetricsByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, includeTargets bool) (RecordingTargetAverages, error) {
var row struct {
HenDayTotal float64
HenHouseTotal float64
EggWeightTotal float64
EggMassTotal float64
FeedIntakeTotal float64
FcrTotal float64
CumDepletionRateTotal float64
TotalCount int64
}
selectParts := []string{
"COALESCE(SUM(feed_intake), 0) AS feed_intake_total",
"COALESCE(SUM(fcr_value), 0) AS fcr_total",
"COALESCE(SUM(cum_depletion_rate), 0) AS cum_depletion_rate_total",
"COUNT(*) AS total_count",
}
if includeTargets {
selectParts = append([]string{
"COALESCE(SUM(hen_day), 0) AS hen_day_total",
"COALESCE(SUM(hen_house), 0) AS hen_house_total",
"COALESCE(SUM(egg_weight), 0) AS egg_weight_total",
"COALESCE(SUM(egg_mass), 0) AS egg_mass_total",
}, selectParts...)
}
if err := r.DB().WithContext(ctx).
Table("recordings").
Select(strings.Join(selectParts, ", ")).
Where("project_flock_kandangs_id = ? AND deleted_at IS NULL", projectFlockKandangID).
Scan(&row).Error; err != nil {
return RecordingTargetAverages{}, err
}
result := RecordingTargetAverages{
FeedIntakeCount: row.TotalCount,
FcrCount: row.TotalCount,
CumDepletionRateCount: row.TotalCount,
}
if includeTargets {
result.HenDayCount = row.TotalCount
result.HenHouseCount = row.TotalCount
result.EggWeightCount = row.TotalCount
result.EggMassCount = row.TotalCount
}
if row.TotalCount > 0 {
if includeTargets {
result.HenDayAvg = row.HenDayTotal / float64(row.TotalCount)
result.HenHouseAvg = row.HenHouseTotal / float64(row.TotalCount)
result.EggWeightAvg = row.EggWeightTotal / float64(row.TotalCount)
result.EggMassAvg = row.EggMassTotal / float64(row.TotalCount)
}
result.FeedIntakeAvg = row.FeedIntakeTotal / float64(row.TotalCount)
result.FcrAvg = row.FcrTotal / float64(row.TotalCount)
result.CumDepletionRateAvg = row.CumDepletionRateTotal
}
return result, nil
}
func nextRecordingDay(days []int) int {
if len(days) == 0 {
return 1
@@ -10,7 +10,6 @@ import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
sProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/services"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
@@ -54,7 +53,6 @@ type recordingService struct {
ProjectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
ApprovalRepo commonRepo.ApprovalRepository
ApprovalSvc commonSvc.ApprovalService
ProductionStandardSvc sProductionStandard.ProductionStandardService
FifoSvc commonSvc.FifoService
}
@@ -66,7 +64,6 @@ func NewRecordingService(
approvalRepo commonRepo.ApprovalRepository,
approvalSvc commonSvc.ApprovalService,
fifoSvc commonSvc.FifoService,
productionStandardSvc sProductionStandard.ProductionStandardService,
validate *validator.Validate,
) RecordingService {
return &recordingService{
@@ -78,7 +75,6 @@ func NewRecordingService(
ProjectFlockPopulationRepo: projectFlockPopulationRepo,
ApprovalRepo: approvalRepo,
ApprovalSvc: approvalSvc,
ProductionStandardSvc: productionStandardSvc,
FifoSvc: fifoSvc,
}
}
@@ -173,14 +169,6 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
}
ctx := c.Context()
recordTime := time.Now().UTC()
if req.RecordDate != nil && strings.TrimSpace(*req.RecordDate) != "" {
parsed, err := time.Parse("2006-01-02", strings.TrimSpace(*req.RecordDate))
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "record_date must be in YYYY-MM-DD format")
}
recordTime = parsed.UTC()
}
pfk, err := s.ProjectFlockKandangRepo.GetByID(ctx, req.ProjectFlockKandangId)
if err != nil {
@@ -200,11 +188,6 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
if err := s.ensureChickInExists(ctx, pfk.Id); err != nil {
return nil, err
}
if s.ProductionStandardSvc != nil {
if err := s.ProductionStandardSvc.EnsureWeekStart(ctx, pfk.ProjectFlock.ProductionStandardId, category); err != nil {
return nil, err
}
}
if !isLaying && len(req.Eggs) > 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
@@ -227,12 +210,8 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
s.Log.Errorf("Failed to determine recording day: %+v", err)
return err
}
if s.ProductionStandardSvc != nil {
if err := s.ProductionStandardSvc.EnsureWeekAvailable(ctx, pfk.ProjectFlock.ProductionStandardId, category, nextDay); err != nil {
return err
}
}
recordTime := time.Now().UTC()
existsToday, err := s.Repository.ExistsOnDate(ctx, req.ProjectFlockKandangId, recordTime)
if err != nil {
s.Log.Errorf("Failed to verify existing recording on date: %+v", err)
@@ -1178,7 +1157,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
var fcrValue float64
if usageInGrams > 0 && totalEggWeightGrams > 0 {
fcrValue = usageInGrams / totalEggWeightGrams
fcrValue = totalEggWeightGrams / usageInGrams
updates["fcr_value"] = fcrValue
recording.FcrValue = &fcrValue
} else {
@@ -1351,16 +1330,12 @@ func (s *recordingService) attachProductionStandard(ctx context.Context, item *e
return nil
}
category := strings.ToUpper(item.ProjectFlockKandang.ProjectFlock.Category)
weekBase := 1
if category == string(utils.ProjectFlockCategoryLaying) {
weekBase = 18
}
week := ((int(*item.Day) - 1) / 7) + weekBase
week := ((int(*item.Day) - 1) / 7) + 1
if week <= 0 {
return nil
}
category := strings.ToUpper(item.ProjectFlockKandang.ProjectFlock.Category)
db := s.Repository.DB()
standardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
growthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
@@ -21,7 +21,6 @@ type (
type Create struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
RecordDate *string `json:"record_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
Stocks []Stock `json:"stocks" validate:"dive"`
Depletions []Depletion `json:"depletions" validate:"dive"`
Eggs []Egg `json:"eggs" validate:"omitempty,dive"`
@@ -84,7 +84,7 @@ func (u *TransferLayingController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Format permintaan tidak valid")
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.TransferLayingService.CreateOne(c, req)
@@ -96,7 +96,7 @@ func (u *TransferLayingController) CreateOne(c *fiber.Ctx) error {
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Berhasil membuat transfer laying",
Message: "Create transferLaying successfully",
Data: dto.ToTransferLayingListDTO(*result),
})
}
@@ -67,6 +67,8 @@ type TransferLayingListDTO struct {
TransferLayingRelationDTO
FromProjectFlock *ProjectFlockSummaryDTO `json:"from_project_flock,omitempty"`
ToProjectFlock *ProjectFlockSummaryDTO `json:"to_project_flock,omitempty"`
PendingUsageQty *float64 `json:"pending_usage_qty"`
UsageQty *float64 `json:"usage_qty"`
CreatedBy uint `json:"created_by"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
@@ -164,7 +166,7 @@ func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouse
func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO {
return LayingTransferSourceDTO{
SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang),
Qty: source.UsageQty, // Ambil dari UsageQty (FIFO consumed quantity)
Qty: source.Qty,
ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse),
Note: source.Note,
}
@@ -184,7 +186,7 @@ func ToLayingTransferSourceDTOs(sources []entity.LayingTransferSource) []LayingT
func ToLayingTransferTargetDTO(target entity.LayingTransferTarget) LayingTransferTargetDTO {
return LayingTransferTargetDTO{
TargetProjectFlockKandang: ToProjectFlockKandangSummaryDTO(target.TargetProjectFlockKandang),
Qty: target.TotalQty, // Ambil dari TotalQty (FIFO replenished quantity)
Qty: target.Qty,
ProductWarehouse: ToProductWarehouseSummaryDTO(target.ProductWarehouse),
Note: target.Note,
}
@@ -221,6 +223,8 @@ func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO {
TransferLayingRelationDTO: ToTransferLayingRelationDTO(e),
FromProjectFlock: ToProjectFlockSummaryDTO(e.FromProjectFlock),
ToProjectFlock: ToProjectFlockSummaryDTO(e.ToProjectFlock),
PendingUsageQty: e.PendingUsageQty,
UsageQty: e.UsageQty,
CreatedBy: e.CreatedBy,
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
@@ -26,8 +26,6 @@ type TransferLayingModule struct{}
func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
transferLayingRepo := rTransferLaying.NewTransferLayingRepository(db)
layingTransferSourceRepo := rTransferLaying.NewLayingTransferSourceRepository(db)
layingTransferTargetRepo := rTransferLaying.NewLayingTransferTargetRepository(db)
userRepo := rUser.NewUserRepository(db)
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
@@ -38,13 +36,30 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
// daftarin jadi stockable
if err := fifoService.RegisterStockable(fifo.StockableConfig{
Key: fifo.StockableKeyTransferToLayingIn,
Table: "laying_transfer_targets",
Columns: fifo.StockableColumns{
if err := fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKeyTransferToLaying,
Table: "laying_transfers",
Columns: fifo.UsableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
UsageQuantity: "usage_qty",
PendingQuantity: "pending_usage_qty",
CreatedAt: "created_at",
},
}); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
panic(fmt.Sprintf("failed to register transfer to laying usable workflow: %v", err))
}
}
if err := fifoService.RegisterStockable(fifo.StockableConfig{
Key: fifo.StockableKeyTransferToLaying,
Table: "laying_transfers",
Columns: fifo.StockableColumns{
ID: "id",
ProductWarehouseID: "dest_product_warehouse_id",
TotalQuantity: "total_qty",
TotalUsedQuantity: "total_used",
CreatedAt: "created_at",
@@ -56,24 +71,6 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
}
}
// daftarin jadi usable
if err := fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKeyTransferToLayingOut,
Table: "laying_transfer_sources",
Columns: fifo.UsableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
UsageQuantity: "usage_qty",
PendingQuantity: "pending_usage_qty",
CreatedAt: "created_at",
},
OrderBy: []string{"created_at ASC", "id ASC"},
}); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
panic(fmt.Sprintf("failed to register transfer to laying usable workflow: %v", err))
}
}
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowTransferToLaying, utils.TransferToLayingApprovalSteps); err != nil {
@@ -82,8 +79,6 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
transferLayingService := sTransferLaying.NewTransferLayingService(
transferLayingRepo,
layingTransferSourceRepo,
layingTransferTargetRepo,
projectFlockRepo,
projectFlockKandangRepo,
projectFlockPopulationRepo,
@@ -16,7 +16,6 @@ import (
ProjectFlockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
@@ -41,22 +40,17 @@ type transferLayingService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.TransferLayingRepository
LayingTransferSourceRepo repository.LayingTransferSourceRepository
LayingTransferTargetRepo repository.LayingTransferTargetRepository
ProjectFlockRepo ProjectFlockRepository.ProjectflockRepository
ProjectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository
ProjectFlockPopulationRepo ProjectFlockRepository.ProjectFlockPopulationRepository
ProductWarehouseRepo rInventory.ProductWarehouseRepository
WarehouseRepo rWarehouse.WarehouseRepository
StockLogRepo rStockLogs.StockLogRepository
ApprovalService commonSvc.ApprovalService
FifoSvc commonSvc.FifoService
}
func NewTransferLayingService(
repo repository.TransferLayingRepository,
layingTransferSourceRepo repository.LayingTransferSourceRepository,
layingTransferTargetRepo repository.LayingTransferTargetRepository,
projectFlockRepo ProjectFlockRepository.ProjectflockRepository,
projectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository,
projectFlockPopulationRepo ProjectFlockRepository.ProjectFlockPopulationRepository,
@@ -70,14 +64,11 @@ func NewTransferLayingService(
Log: utils.Log,
Validate: validate,
Repository: repo,
LayingTransferSourceRepo: layingTransferSourceRepo,
LayingTransferTargetRepo: layingTransferTargetRepo,
ProjectFlockRepo: projectFlockRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
ProjectFlockPopulationRepo: projectFlockPopulationRepo,
ProductWarehouseRepo: productWarehouseRepo,
WarehouseRepo: warehouseRepo,
StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()),
ApprovalService: approvalService,
FifoSvc: fifoSvc,
}
@@ -173,42 +164,55 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
return nil, err
}
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Source Project Flock", ID: &req.SourceProjectFlockId, Exists: s.ProjectFlockRepo.IdExists},
commonSvc.RelationCheck{Name: "Target Project Flock", ID: &req.TargetProjectFlockId, Exists: s.ProjectFlockRepo.IdExists},
); err != nil {
return nil, err
if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.SourceProjectFlockId, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Source Project Flock not found")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate source project flock")
}
sourceKandangIDs := make([]uint, len(req.SourceKandangs))
for i, detail := range req.SourceKandangs {
sourceKandangIDs[i] = detail.ProjectFlockKandangId
if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.TargetProjectFlockId, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Target Project Flock not found")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate target project flock")
}
if err := s.validateKandangOwnership(
c.Context(),
req.SourceProjectFlockId,
sourceKandangIDs,
); err != nil {
return nil, err
for _, detail := range req.SourceKandangs {
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Source Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists},
); err != nil {
return nil, err
}
pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), detail.ProjectFlockKandangId)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get source project flock kandang")
}
if pfk.ProjectFlockId != req.SourceProjectFlockId {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d does not belong to source project flock %d", detail.ProjectFlockKandangId, req.SourceProjectFlockId))
}
}
targetKandangIDs := make([]uint, len(req.TargetKandangs))
for i, detail := range req.TargetKandangs {
targetKandangIDs[i] = detail.ProjectFlockKandangId
}
for _, detail := range req.TargetKandangs {
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Target Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists},
); err != nil {
return nil, err
}
if err := s.validateKandangOwnership(
c.Context(),
req.TargetProjectFlockId,
targetKandangIDs,
); err != nil {
return nil, err
pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), detail.ProjectFlockKandangId)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang")
}
if pfk.ProjectFlockId != req.TargetProjectFlockId {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Target kandang %d does not belong to target project flock %d", detail.ProjectFlockKandangId, req.TargetProjectFlockId))
}
}
transferDate, err := utils.ParseDateString(req.TransferDate)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Format tanggal transfer tidak valid")
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transfer date format")
}
var totalSourceQty, totalTargetQty float64
@@ -216,7 +220,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
for _, sourceDetail := range req.SourceKandangs {
if sourceDetail.Quantity <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang sumber harus lebih dari 0")
return nil, fiber.NewError(fiber.StatusBadRequest, "Source kandang quantity must be greater than 0")
}
totalSourceQty += sourceDetail.Quantity
@@ -235,11 +239,11 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
}
if totalPopulation == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang sumber %d tidak memiliki populasi untuk ditransfer", sourceDetail.ProjectFlockKandangId))
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no population available for transfer", sourceDetail.ProjectFlockKandangId))
}
if totalPopulation < sourceDetail.Quantity {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang sumber %d jumlah tidak mencukupi. Tersedia: %.0f, Diminta: %.0f", sourceDetail.ProjectFlockKandangId, totalPopulation, sourceDetail.Quantity))
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has insufficient quantity. Available: %.0f, Requested: %.0f", sourceDetail.ProjectFlockKandangId, totalPopulation, sourceDetail.Quantity))
}
sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] = productWarehouseId
@@ -247,13 +251,13 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
for _, targetDetail := range req.TargetKandangs {
if targetDetail.Quantity <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang tujuan harus lebih dari 0")
return nil, fiber.NewError(fiber.StatusBadRequest, "Target kandang quantity must be greater than 0")
}
totalTargetQty += targetDetail.Quantity
}
if totalSourceQty != totalTargetQty {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Jumlah total sumber (%.0f) harus sama dengan jumlah total tujuan (%.0f)", totalSourceQty, totalTargetQty))
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total source quantity (%f) must equal total target quantity (%f)", totalSourceQty, totalTargetQty))
}
transferNumber := fmt.Sprintf("TL-%d", time.Now().UnixNano())
@@ -264,18 +268,22 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
FromProjectFlockId: req.SourceProjectFlockId,
ToProjectFlockId: req.TargetProjectFlockId,
TransferDate: transferDate,
PendingUsageQty: &totalSourceQty,
CreatedBy: actorID,
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
if len(sourceWarehouseMap) > 0 {
for _, pwID := range sourceWarehouseMap {
createBody.ProductWarehouseId = &pwID
break
}
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
repoTx := s.Repository.WithTx(dbTransaction)
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
pwRepoTx := rInventory.NewProductWarehouseRepository(dbTransaction)
if err := repoTx.CreateOne(c.Context(), createBody, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat record transfer laying")
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying record")
}
for _, sourceDetail := range req.SourceKandangs {
@@ -284,88 +292,78 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
source := entity.LayingTransferSource{
LayingTransferId: createBody.Id,
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
UsageQty: 0,
PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval
Qty: sourceDetail.Quantity,
ProductWarehouseId: &productWarehouseId,
}
if err := sourceRepoTx.CreateOne(c.Context(), &source, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat sumber transfer")
if err := dbTransaction.Create(&source).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer source")
}
}
for _, targetDetail := range req.TargetKandangs {
var firstTargetProductWarehouseID *uint
targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
for i, targetDetail := range req.TargetKandangs {
targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mendapatkan project flock kandang tujuan")
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang")
}
targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetprojectFlockKandang.KandangId)
targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse tidak ditemukan untuk kandang tujuan %d", targetDetail.ProjectFlockKandangId))
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No warehouse found for target kandang %d", targetDetail.ProjectFlockKandangId))
}
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mendapatkan warehouse tujuan")
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse")
}
// Ambil product ID dari salah satu source warehouse (harusnya semua sources product-nya sama)
var sourceProductID uint
for _, sourceDetail := range req.SourceKandangs {
if pwID, ok := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId]; ok {
// Get product warehouse untuk ambil product ID
sourcePW, err := pwRepoTx.GetByID(c.Context(), pwID, nil)
if err == nil {
sourceProductID = sourcePW.ProductId
break
}
}
}
if sourceProductID == 0 {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mendapatkan product dari source warehouse")
}
// Cari product warehouse di target berdasarkan: warehouse + project_flock_kandang + PRODUCT
targetPW, err := pwRepoTx.FindByProductWarehouseAndPfk(c.Context(), sourceProductID, targetWarehouse.Id, &targetDetail.ProjectFlockKandangId)
var targetPW entity.ProductWarehouse
err = dbTransaction.Where("warehouse_id = ? AND project_flock_kandang_id = ?", targetWarehouse.Id, targetDetail.ProjectFlockKandangId).
First(&targetPW).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
newTargetPW := entity.ProductWarehouse{
ProductId: sourceProductID,
WarehouseId: targetWarehouse.Id,
ProjectFlockKandangId: &targetDetail.ProjectFlockKandangId,
Quantity: 0,
}
if err := pwRepoTx.CreateOne(c.Context(), &newTargetPW, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal membuat product warehouse untuk kandang tujuan %d: %v", targetDetail.ProjectFlockKandangId, err))
}
targetPW = &newTargetPW
} else {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mendapatkan product warehouse untuk kandang tujuan %d: %v", targetDetail.ProjectFlockKandangId, err))
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No product warehouse found for target kandang %d in warehouse %d", targetDetail.ProjectFlockKandangId, targetWarehouse.Id))
}
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get product warehouse for target kandang %d: %v", targetDetail.ProjectFlockKandangId, err))
}
target := entity.LayingTransferTarget{
LayingTransferId: createBody.Id,
TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId,
TotalQty: targetDetail.Quantity,
TotalUsed: 0,
Qty: targetDetail.Quantity,
ProductWarehouseId: &targetPW.Id,
}
if err := targetRepoTx.CreateOne(c.Context(), &target, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat target transfer")
if err := dbTransaction.Create(&target).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer target")
}
if i == 0 {
firstTargetProductWarehouseID = &targetPW.Id
}
}
// Set DestProductWarehouseID untuk STOCKABLE role (ambil dari target pertama)
if firstTargetProductWarehouseID != nil {
createBody.DestProductWarehouseID = firstTargetProductWarehouseID
// Update DestProductWarehouseID ke database
if err := dbTransaction.Model(&entity.LayingTransfer{}).
Where("id = ?", createBody.Id).
Update("dest_product_warehouse_id", *firstTargetProductWarehouseID).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update DestProductWarehouseID")
}
}
if err := createApprovalTransferLaying(c.Context(), dbTransaction, createBody.Id, createBody.CreatedBy); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat approval transfer")
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer approval")
}
return nil
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat transfer laying")
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying")
}
return s.GetOne(c, createBody.Id)
@@ -414,32 +412,53 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
repoTx := s.Repository.WithTx(dbTransaction)
sourceRepo := s.LayingTransferSourceRepo.WithTx(dbTransaction)
targetRepo := s.LayingTransferTargetRepo.WithTx(dbTransaction)
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
// Hapus old sources dan targets
for _, oldSource := range existingTransfer.Sources {
if err := sourceRepo.DeleteOne(c.Context(), oldSource.Id); err != nil {
if oldSource.ProductWarehouseId != nil && oldSource.Qty > 0 {
if err := productWarehouseRepoTx.PatchOne(c.Context(), *oldSource.ProductWarehouseId, map[string]any{
"qty": gorm.Expr("qty + ?", oldSource.Qty),
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore warehouse quantity")
}
if err := s.restoreProjectFlockPopulation(c.Context(), projectFlockPopulationRepoTx, oldSource.SourceProjectFlockKandangId, oldSource.Qty); err != nil {
return err
}
}
}
for _, oldSource := range existingTransfer.Sources {
if err := dbTransaction.Delete(&oldSource).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete old source")
}
}
for _, oldTarget := range existingTransfer.Targets {
if err := targetRepo.DeleteOne(c.Context(), oldTarget.Id); err != nil {
if err := dbTransaction.Delete(&oldTarget).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete old target")
}
}
totalSourceQty := 0.0
for _, source := range req.SourceKandangs {
totalSourceQty += source.Quantity
}
if err := repoTx.PatchOne(c.Context(), id, map[string]any{
"transfer_date": transferDate,
"notes": req.Reason,
"transfer_date": transferDate,
"notes": req.Reason,
"pending_usage_qty": &totalSourceQty,
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer header")
}
// Create new sources dengan pending quantity
sourceWarehouseMap := make(map[uint]uint)
for _, sourceDetail := range req.SourceKandangs {
populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId)
populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations")
}
@@ -448,39 +467,48 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no population available", sourceDetail.ProjectFlockKandangId))
}
var totalPopulation float64
var productWarehouseId uint
for _, pop := range populations {
totalPopulation += pop.TotalQty
if pop.ProductWarehouseId > 0 {
productWarehouseId = pop.ProductWarehouseId
break
}
}
if productWarehouseId == 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no product warehouse", sourceDetail.ProjectFlockKandangId))
if totalPopulation < sourceDetail.Quantity {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has insufficient quantity. Available: %.0f, Requested: %.0f", sourceDetail.ProjectFlockKandangId, totalPopulation, sourceDetail.Quantity))
}
sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] = productWarehouseId
source := entity.LayingTransferSource{
LayingTransferId: id,
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
UsageQty: 0,
PendingUsageQty: sourceDetail.Quantity,
Qty: sourceDetail.Quantity,
ProductWarehouseId: &productWarehouseId,
}
if err := sourceRepo.CreateOne(c.Context(), &source, nil); err != nil {
if err := dbTransaction.Create(&source).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer source")
}
if err := s.reduceProjectFlockPopulation(c.Context(), projectFlockPopulationRepoTx, sourceDetail.ProjectFlockKandangId, sourceDetail.Quantity); err != nil {
return err
}
if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouseId, map[string]any{"qty": gorm.Expr("qty - ?", sourceDetail.Quantity)}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update source warehouse quantity")
}
}
pwRepo := rInventory.NewProductWarehouseRepository(dbTransaction)
for _, targetDetail := range req.TargetKandangs {
targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang")
}
targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetprojectFlockKandang.KandangId)
targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No warehouse found for target kandang %d", targetDetail.ProjectFlockKandangId))
@@ -488,50 +516,23 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse")
}
// Ambil product ID dari source yang pertama (semua sources seharusnya product-nya sama)
var sourceProductID uint
if len(req.SourceKandangs) > 0 {
firstSourceKandangID := req.SourceKandangs[0].ProjectFlockKandangId
populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), firstSourceKandangID)
if err == nil && len(populations) > 0 && populations[0].ProductWarehouseId > 0 {
sourcePW, err := pwRepo.GetByID(c.Context(), populations[0].ProductWarehouseId, nil)
if err == nil {
sourceProductID = sourcePW.ProductId
}
}
}
if sourceProductID == 0 {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product from source warehouse")
}
targetPW, err := pwRepo.FindByProductWarehouseAndPfk(c.Context(), sourceProductID, targetWarehouse.Id, &targetDetail.ProjectFlockKandangId)
var targetPW entity.ProductWarehouse
err = dbTransaction.Where("warehouse_id = ? AND project_flock_kandang_id = ?", targetWarehouse.Id, targetDetail.ProjectFlockKandangId).
First(&targetPW).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
newTargetPW := entity.ProductWarehouse{
ProductId: sourceProductID,
WarehouseId: targetWarehouse.Id,
ProjectFlockKandangId: &targetDetail.ProjectFlockKandangId,
Quantity: 0,
}
if err := pwRepo.CreateOne(c.Context(), &newTargetPW, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to create product warehouse for target kandang %d: %v", targetDetail.ProjectFlockKandangId, err))
}
targetPW = &newTargetPW
} else {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get product warehouse for target kandang %d: %v", targetDetail.ProjectFlockKandangId, err))
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No product warehouse found for target kandang %d in warehouse %d", targetDetail.ProjectFlockKandangId, targetWarehouse.Id))
}
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get product warehouse for target kandang %d: %v", targetDetail.ProjectFlockKandangId, err))
}
target := entity.LayingTransferTarget{
LayingTransferId: id,
TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId,
TotalQty: targetDetail.Quantity,
TotalUsed: 0,
Qty: targetDetail.Quantity,
ProductWarehouseId: &targetPW.Id,
}
if err := targetRepo.CreateOne(c.Context(), &target, nil); err != nil {
if err := dbTransaction.Create(&target).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer target")
}
}
@@ -559,7 +560,6 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
}
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
@@ -573,6 +573,48 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
repoTx := s.Repository.WithTx(dbTransaction)
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer sources")
}
for _, source := range sources {
if source.ProductWarehouseId != nil && source.Qty > 0 {
if err := productWarehouseRepoTx.PatchOne(c.Context(), *source.ProductWarehouseId, map[string]any{
"qty": gorm.Expr("qty + ?", source.Qty),
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore source warehouse quantity")
}
}
}
for _, source := range sources {
populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), source.SourceProjectFlockKandangId)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations for restoration")
}
remainingToRestore := source.Qty
for i := len(populations) - 1; i >= 0 && remainingToRestore > 0; i-- {
pop := populations[i]
restoreAmount := remainingToRestore
if pop.TotalQty < remainingToRestore {
restoreAmount = pop.TotalQty
}
newQty := pop.TotalQty + restoreAmount
if err := projectFlockPopulationRepoTx.PatchOne(c.Context(), pop.Id, map[string]any{"total_qty": newQty}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore population quantity")
}
remainingToRestore -= restoreAmount
}
}
if err := repoTx.DeleteOne(c.Context(), id); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying")
@@ -625,8 +667,6 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
repoTx := s.Repository.WithTx(dbTransaction)
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
// Gunakan repo baru untuk transaction scope agar bisa akses method custom
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
@@ -651,73 +691,70 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
return fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
}
if action == entity.ApprovalActionApproved {
if action == entity.ApprovalActionApproved && transfer.PendingUsageQty != nil && *transfer.PendingUsageQty > 0 {
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), approvableID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil sources transfer")
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer sources")
}
targets, err := targetRepoTx.GetByLayingTransferId(c.Context(), approvableID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil targets transfer")
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer targets")
}
// Hitung total quantity dari targets untuk di-consume dari sources
totalTargetQty := 0.0
for _, target := range targets {
totalTargetQty += target.TotalQty
}
if len(sources) > 0 && len(targets) > 0 {
// Consume dari laying_transfer_sources (Usable) - akan consume dari ProjectFlockPopulation (Stockable)
for _, source := range sources {
if source.ProductWarehouseId == nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse tidak ditemukan untuk transfer %d", approvableID))
for _, source := range sources {
if source.ProductWarehouseId == nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse not found for transfer %d", approvableID))
}
_, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
UsableKey: fifo.UsableKeyTransferToLaying,
UsableID: approvableID,
ProductWarehouseID: *source.ProductWarehouseId,
Quantity: source.Qty,
AllowPending: false,
Tx: dbTransaction,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to consume FIFO stock for source %d: %v", source.ProductWarehouseId, err))
}
}
consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
UsableKey: fifo.UsableKeyTransferToLayingOut,
UsableID: source.Id,
ProductWarehouseID: *source.ProductWarehouseId,
Quantity: totalTargetQty,
AllowPending: false,
Tx: dbTransaction,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal consume FIFO stock: %v", err))
}
if transfer.DestProductWarehouseID != nil {
note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber)
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyTransferToLaying,
StockableID: approvableID,
ProductWarehouseID: *transfer.DestProductWarehouseID,
Quantity: *transfer.PendingUsageQty,
Note: &note,
Tx: dbTransaction,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock to destination warehouse: %v", err))
}
if err := sourceRepoTx.PatchOne(c.Context(), source.Id, map[string]interface{}{
"usage_qty": source.UsageQty + consumeResult.UsageQuantity,
"pending_usage_qty": consumeResult.PendingQuantity,
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal update source usage qty")
if err := dbTransaction.Model(&entity.LayingTransfer{}).
Where("id = ?", approvableID).
Updates(map[string]interface{}{
"total_qty": replenishResult.AddedQuantity,
}).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update total quantity for transfer")
}
}
}
for _, target := range targets {
if target.ProductWarehouseId == nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID))
}
note := fmt.Sprintf("Transfer to Laying #%s - Target Kandang", transfer.TransferNumber)
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyTransferToLayingIn,
StockableID: target.Id,
ProductWarehouseID: *target.ProductWarehouseId,
Quantity: target.TotalQty,
Note: &note,
Tx: dbTransaction,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal replenish stock ke target warehouse: %v", err))
}
if err := targetRepoTx.PatchOne(c.Context(), target.Id, map[string]interface{}{
"total_qty": replenishResult.AddedQuantity,
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal update target total qty")
}
usageQty := *transfer.PendingUsageQty
updateData := map[string]any{
"usage_qty": usageQty,
"total_qty": usageQty, // Same as usage_qty for initial transfer
"pending_usage_qty": nil,
}
if err := repoTx.PatchOne(c.Context(), approvableID, updateData, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer laying status")
}
}
}
@@ -783,8 +820,9 @@ func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context,
newWarehouse := &entity.ProductWarehouse{
ProductId: productID,
WarehouseId: warehouseID,
ProjectFlockKandangId: projectFlockKandangId,
ProjectFlockKandangId: projectFlockKandangId, // Set flock ID agar bisa di-chickin di target flock
Quantity: quantity,
// CreatedBy: actorID,
}
if err := productWarehouseRepoTx.CreateOne(ctx, newWarehouse, nil); err != nil {
@@ -794,6 +832,66 @@ func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context,
return newWarehouse, nil
}
func (s *transferLayingService) reduceProjectFlockPopulation(ctx context.Context, populationRepo ProjectFlockRepository.ProjectFlockPopulationRepository, projectFlockKandangID uint, quantityToReduce float64) error {
populations, err := populationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
if err != nil {
return err
}
if len(populations) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "No populations found for reduction")
}
remainingToReduce := quantityToReduce
for i := len(populations) - 1; i >= 0; i-- {
if remainingToReduce <= 0 {
break
}
pop := populations[i]
reductionAmount := remainingToReduce
if pop.TotalQty < remainingToReduce {
reductionAmount = pop.TotalQty
}
newQty := pop.TotalQty - reductionAmount
if err := populationRepo.PatchOne(ctx, pop.Id, map[string]any{"total_qty": newQty}, nil); err != nil {
return err
}
remainingToReduce -= reductionAmount
}
if remainingToReduce > 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient population to reduce. Still need to reduce: %.0f", remainingToReduce))
}
return nil
}
func (s *transferLayingService) restoreProjectFlockPopulation(ctx context.Context, populationRepo ProjectFlockRepository.ProjectFlockPopulationRepository, projectFlockKandangID uint, quantityToRestore float64) error {
populations, err := populationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
if err != nil {
return err
}
if len(populations) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "No populations found for restoration")
}
if len(populations) > 0 {
lastPop := populations[len(populations)-1]
newQty := lastPop.TotalQty + quantityToRestore
if err := populationRepo.PatchOne(ctx, lastPop.Id, map[string]any{"total_qty": newQty}, nil); err != nil {
return err
}
}
return nil
}
func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) {
pf, err := s.ProjectFlockRepo.GetByID(ctx.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
@@ -827,27 +925,3 @@ func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, project
return pf, kandangAvailableQty, nil
}
func (s *transferLayingService) validateKandangOwnership(
ctx context.Context,
projectFlockID uint,
kandangIDs []uint,
) error {
for _, kandangID := range kandangIDs {
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(ctx, kandangID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang %d tidak ditemukan", kandangID))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get project flock kandang")
}
if projectFlockKandang.ProjectFlockId != projectFlockID {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d tidak terhubung ke project flock %d", kandangID, projectFlockID))
}
}
return nil
}
@@ -285,7 +285,7 @@ func (r *PurchaseRepositoryImpl) generateSequentialNumber(ctx context.Context, t
var values []string
err := db.WithContext(ctx).
Model(&entity.Purchase{}).
Where(fmt.Sprintf("%s ILIKE ?", column), prefix+"%").
Where(fmt.Sprintf("%s LIKE ?", column), prefix+"%").
Select(column).
Order(fmt.Sprintf("%s DESC", column)).
Limit(20).
+8 -8
View File
@@ -15,12 +15,12 @@ func Routes(router fiber.Router, purchaseService service.PurchaseService, userSe
route := router.Group("/purchases")
route.Use(m.Auth(userService))
route.Get("/", m.RequirePermissions(m.P_PurchaseGetAll), ctrl.GetAll)
route.Get("/:id", m.RequirePermissions(m.P_PurchaseGetOne), ctrl.GetOne)
route.Post("/", m.RequirePermissions(m.P_PurchaseCreateOne), ctrl.CreateOne)
route.Post("/:id/approvals/staff", m.RequirePermissions(m.P_PurchaseApprovalStaff), ctrl.ApproveStaffPurchase)
route.Post("/:id/approvals/manager", m.RequirePermissions(m.P_PurchaseApprovalManager), ctrl.ApproveManagerPurchase)
route.Post("/:id/receipts", m.RequirePermissions(m.P_PurchaseReceive), ctrl.ReceiveProducts)
route.Delete("/:id", m.RequirePermissions(m.P_PurchaseDeleteOne), ctrl.DeletePurchase)
route.Delete("/:id/items", m.RequirePermissions(m.P_PurchaseItemDeleteOne), ctrl.DeleteItems)
route.Get("/",m.RequirePermissions(m.P_PurchaseGetAll), ctrl.GetAll)
route.Get("/:id",m.RequirePermissions(m.P_PurchaseGetOne), ctrl.GetOne)
route.Post("/", ctrl.CreateOne)
route.Post("/:id/approvals/staff", ctrl.ApproveStaffPurchase)
route.Post("/:id/approvals/manager", ctrl.ApproveManagerPurchase)
route.Post("/:id/receipts",ctrl.ReceiveProducts)
route.Delete("/:id", ctrl.DeletePurchase)
route.Delete("/:id/items", ctrl.DeleteItems)
}
@@ -618,10 +618,7 @@ func (b *expenseBridge) createExpenseViaService(
actorID = 1
}
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepHeadArea, &action, actorID, nil); err != nil {
return nil, err
}
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepUnitVicePresident, &action, actorID, nil); err != nil {
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepManager, &action, actorID, nil); err != nil {
return nil, err
}
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
@@ -3,7 +3,6 @@ package controller
import (
"math"
"strconv"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services"
@@ -82,7 +81,6 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
ProductId: int64(ctx.QueryInt("product_id", 0)),
WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)),
SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)),
MarketingType: ctx.Query("marketing_type", ""),
FilterBy: ctx.Query("filter_by", ""),
StartDate: ctx.Query("start_date", ""),
EndDate: ctx.Query("end_date", ""),
@@ -166,59 +164,6 @@ func (c *RepportController) GetPurchaseSupplier(ctx *fiber.Ctx) error {
})
}
func (c *RepportController) GetDebtSupplier(ctx *fiber.Ctx) error {
supplierIDs, err := parseCommaSeparatedInt64s(ctx.Query("supplier_ids", ""))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
query := &validation.DebtSupplierQuery{
Page: ctx.QueryInt("page", 1),
Limit: ctx.QueryInt("limit", 10),
SupplierIDs: supplierIDs,
StartDate: ctx.Query("start_date", ""),
EndDate: ctx.Query("end_date", ""),
FilterBy: ctx.Query("filter_by", ""),
SortOrder: ctx.Query("sort_order", ""),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := c.RepportService.GetDebtSupplier(ctx, query)
if err != nil {
return err
}
supplierIDs = query.SupplierIDs
if supplierIDs == nil {
supplierIDs = []int64{}
}
filters := map[string]interface{}{
"start_date": query.StartDate,
"end_date": query.EndDate,
"supplier_ids": supplierIDs,
"filter_by": query.FilterBy,
}
return ctx.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.DebtSupplierDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get supplier debt recap successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
Filters: filters,
},
Data: result,
})
}
func (c *RepportController) GetHppPerKandang(ctx *fiber.Ctx) error {
data, meta, err := c.RepportService.GetHppPerKandang(ctx)
if err != nil {
@@ -282,27 +227,3 @@ func (c *RepportController) GetProductionResult(ctx *fiber.Ctx) error {
Data: data,
})
}
func parseCommaSeparatedInt64s(raw string) ([]int64, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return []int64{}, nil
}
parts := strings.Split(raw, ",")
result := make([]int64, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
id, err := strconv.ParseInt(part, 10, 64)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "supplier_ids must be comma separated integers")
}
result = append(result, id)
}
return result, nil
}
@@ -1,39 +0,0 @@
package dto
import (
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
)
type DebtSupplierRowDTO struct {
PrNumber string `json:"pr_number"`
PoNumber string `json:"po_number"`
PoDate string `json:"po_date"`
ReceivedDate string `json:"received_date"`
Aging int `json:"aging"`
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"`
DueDate string `json:"due_date"`
DueStatus string `json:"due_status"`
TotalPrice float64 `json:"total_price"`
PaymentPrice float64 `json:"payment_price"`
DebtPrice float64 `json:"debt_price"`
Status string `json:"status"`
TravelNumber string `json:"travel_number"`
Balance float64 `json:"balance"`
}
type DebtSupplierTotalDTO struct {
Aging int `json:"aging"`
TotalPrice float64 `json:"total_price"`
PaymentPrice float64 `json:"payment_price"`
DebtPrice float64 `json:"debt_price"`
}
type DebtSupplierDTO struct {
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
InitialBalance float64 `json:"initial_balance"`
Rows []DebtSupplierRowDTO `json:"rows"`
Total DebtSupplierTotalDTO `json:"total"`
}
@@ -1,16 +1,12 @@
package dto
import (
"encoding/json"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
marketingDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -26,7 +22,7 @@ type RepportMarketingItemDTO struct {
DoNumber string `json:"do_number"`
Sales *userDTO.UserRelationDTO `json:"sales,omitempty"`
VehicleNumber string `json:"vehicle_number"`
Product *ProductRelationDTOFixed `json:"product,omitempty"`
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
MarketingType string `json:"marketing_type"`
Qty float64 `json:"qty"`
AverageWeightKg float64 `json:"average_weight_kg"`
@@ -50,12 +46,6 @@ type RepportMarketingResponseDTO struct {
Total *Summary `json:"total,omitempty"`
}
type ProductRelationDTOFixed struct {
productDTO.ProductRelationDTO
ProductPrice float64 `json:"product_price"`
SellingPrice *float64 `json:"selling_price,omitempty"`
}
func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) RepportMarketingItemDTO {
soDate := time.Time{}
agingDays := 0
@@ -116,7 +106,7 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK
if mdp.MarketingProduct.ProductWarehouse.ProductId != 0 {
mapped := productDTO.ToProductRelationDTO(mdp.MarketingProduct.ProductWarehouse.Product)
item.Product = newProductRelationDTOFixedPtr(&mapped)
item.Product = &mapped
}
return item
@@ -149,7 +139,7 @@ func ToRepportMarketingItemDTOsWithHppMap(mdps []entity.MarketingDeliveryProduct
}
func getMarketingType(mdp entity.MarketingDeliveryProduct) string {
hasAyam, hasTelur, hasTrading := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
hasAyam, hasTelur := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
if hasAyam {
return "ayam"
@@ -157,15 +147,12 @@ func getMarketingType(mdp entity.MarketingDeliveryProduct) string {
if hasTelur {
return "telur"
}
if hasTrading {
return "trading"
}
return "trading" // default to trading if no flags found
return "trading"
}
func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur, hasTrading bool) {
func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur bool) {
if len(flags) == 0 {
return false, false, false
return false, false
}
for _, flag := range flags {
@@ -180,18 +167,13 @@ func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur, hasTrading bool)
ft == utils.FlagTelurPutih || ft == utils.FlagTelurRetak {
hasTelur = true
}
if ft == utils.FlagOVK || ft == utils.FlagObat || ft == utils.FlagVitamin || ft == utils.FlagKimia ||
ft == utils.FlagPakan || ft == utils.FlagPreStarter || ft == utils.FlagStarter || ft == utils.FlagFinisher {
hasTrading = true
}
}
return hasAyam, hasTelur, hasTrading
return hasAyam, hasTelur
}
func isProductEligibleForHpp(mdp entity.MarketingDeliveryProduct, category string) bool {
hasAyam, hasTelur, _ := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
hasAyam, hasTelur := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing {
return hasAyam
@@ -277,49 +259,3 @@ func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPr
Total: total,
}
}
func newProductRelationDTOFixedPtr(original *productDTO.ProductRelationDTO) *ProductRelationDTOFixed {
if original == nil {
return nil
}
fixed := ProductRelationDTOFixed{
ProductRelationDTO: *original,
ProductPrice: original.ProductPrice,
SellingPrice: original.SellingPrice,
}
return &fixed
}
func (p ProductRelationDTOFixed) MarshalJSON() ([]byte, error) {
type Alias struct {
Id uint `json:"id"`
Name string `json:"name"`
ProductPrice float64 `json:"product_price"`
SellingPrice *float64 `json:"selling_price"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flags *[]string `json:"flags,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
}
suppliers := make([]supplierDTO.SupplierRelationDTO, len(p.ProductRelationDTO.Suppliers))
for i, ps := range p.ProductRelationDTO.Suppliers {
suppliers[i] = supplierDTO.SupplierRelationDTO{
Id: ps.Id,
Name: ps.Name,
Alias: ps.Alias,
Category: ps.Category,
}
}
return json.Marshal(&Alias{
Id: p.ProductRelationDTO.Id,
Name: p.ProductRelationDTO.Name,
ProductPrice: p.ProductPrice,
SellingPrice: p.SellingPrice,
Uom: p.ProductRelationDTO.Uom,
Flags: p.ProductRelationDTO.Flags,
ProductCategory: p.ProductRelationDTO.ProductCategory,
Suppliers: suppliers,
})
}
+1 -2
View File
@@ -31,13 +31,12 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
recordingRepository := recordingRepo.NewRecordingRepository(db)
approvalRepository := commonRepo.NewApprovalRepository(db)
purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db)
debtSupplierRepository := repportRepo.NewDebtSupplierRepository(db)
hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db)
productionResultRepository := repportRepo.NewProductionResultRepository(db)
userRepository := rUser.NewUserRepository(db)
approvalSvc := approvalService.NewApprovalService(approvalRepository)
repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository)
repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, hppPerKandangRepository, productionResultRepository)
userService := sUser.NewUserService(userRepository, validate)
RepportRoutes(router, userService, repportService)

Some files were not shown because too many files have changed in this diff Show More