mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 714bd19d61 | |||
| 2a884a8d09 | |||
| 9fc2d0556e | |||
| c2a89910fb | |||
| 1847e5590a | |||
| 57094f664c | |||
| f8b6e12d16 | |||
| e12c34db13 | |||
| 3012d260ec | |||
| 8a639f127c | |||
| 2acaa10b60 | |||
| bd9d41e161 | |||
| 06d8d0b795 | |||
| 1d5e7b6e1a | |||
| da190f1b05 | |||
| c8905eb715 | |||
| 03fbf7f4b7 | |||
| 7545e9b37d | |||
| 69f38bf16a | |||
| 5730053e04 | |||
| b087a703ef | |||
| 00edcb6add | |||
| de6580d11c | |||
| dcd6008946 | |||
| 711f58abae | |||
| ffd3c905fe | |||
| 6e4a8617da | |||
| 8a57d5d675 | |||
| 5de81f6315 | |||
| e50dd096a4 | |||
| 7551d11888 | |||
| 7444cfac31 | |||
| 1b5b5bc847 | |||
| 5d7b613ffc | |||
| 33e89d65ab | |||
| 0f4cc6e379 | |||
| 590df26a1f | |||
| ce7ce778fd | |||
| eaa208f733 | |||
| b088eebac5 | |||
| 3c10866208 | |||
| f7a392be52 | |||
| 4bd8319e3b | |||
| f7b70d4b14 | |||
| 9f28294dc3 | |||
| 6a166ceb86 | |||
| f37bf4d22d | |||
| ac5edb36e7 | |||
| 8fab5d7d91 | |||
| 5ddfb2c745 | |||
| 5cfa4d4a59 | |||
| 80b2cafd2f | |||
| b47f26d448 | |||
| 2f22182605 | |||
| e2d352721c | |||
| 068fe4329e | |||
| 15be8dcbea | |||
| 041e8763ac | |||
| 644e9911e4 | |||
| bb04cb53d9 | |||
| 048e607290 | |||
| 18441eb19f | |||
| 526e14f26e | |||
| 539081ce99 | |||
| d568b87e01 | |||
| 9515848d8f | |||
| c15ff8a211 | |||
| d1d94357cf | |||
| 67f5165bfb | |||
| 1217f34dcd | |||
| ae41422776 | |||
| 3978951d8f | |||
| 3422fceec7 | |||
| 09f1b29359 | |||
| 167d18fe87 | |||
| 473f4504ea | |||
| dc7dc0ba47 | |||
| a54129866e | |||
| d40243be4b | |||
| 525ff650f2 | |||
| c1e9b5a975 | |||
| 272367d8ef | |||
| d33119661a | |||
| 3d76854273 | |||
| b7a3882f20 | |||
| 29933a5df9 | |||
| f8aee4be7b | |||
| cad91957b3 | |||
| 30231fabe9 | |||
| e738a97e4c | |||
| 81f4a5e33e | |||
| 1e9fdd2b0d | |||
| b6a60d5009 |
+4
-1
@@ -9,11 +9,13 @@ main
|
||||
bin/
|
||||
*.exe
|
||||
*.out
|
||||
|
||||
.air.toml
|
||||
Makefile
|
||||
docker-compose.local.yml
|
||||
docker-compose.yaml
|
||||
Dockerfile
|
||||
Dockerfile.local
|
||||
.gitlab-ci.yml
|
||||
# Go build cache
|
||||
.gocache/
|
||||
vendor
|
||||
@@ -27,3 +29,4 @@ coverage/
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
.DS_Store
|
||||
|
||||
+29
-11
@@ -1,20 +1,38 @@
|
||||
FROM golang:1.23-alpine
|
||||
# =========================
|
||||
# Builder stage
|
||||
# =========================
|
||||
FROM golang:1.23-alpine AS builder
|
||||
|
||||
# Install dependensi dasar
|
||||
RUN apk add --no-cache git curl bash build-base
|
||||
RUN apk add --no-cache git ca-certificates tzdata
|
||||
WORKDIR /app
|
||||
|
||||
# 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 ["air", "-c", ".air.toml"]
|
||||
CMD ["/app/lti-api"]
|
||||
|
||||
@@ -108,6 +108,6 @@ internal/
|
||||
|
||||
IT Development PT Mitra Berlian Unggas Group
|
||||
|
||||
## 📃 License
|
||||
## 📃 Licensee
|
||||
|
||||
This project is private. All rights reserved.
|
||||
> This project is private. All rights reserved.
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
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
|
||||
@@ -1,98 +0,0 @@
|
||||
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:
|
||||
@@ -16,6 +16,7 @@ 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
|
||||
@@ -60,7 +61,6 @@ 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
|
||||
|
||||
@@ -262,14 +262,10 @@ 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=
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
-- 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';
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
-- 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;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Rollback: remove price from supplier relations
|
||||
ALTER TABLE product_suppliers
|
||||
DROP COLUMN IF EXISTS price;
|
||||
|
||||
ALTER TABLE nonstock_suppliers
|
||||
DROP COLUMN IF EXISTS price;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 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;
|
||||
@@ -0,0 +1,18 @@
|
||||
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"`
|
||||
}
|
||||
@@ -12,17 +12,6 @@ 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"`
|
||||
@@ -31,8 +20,6 @@ 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"`
|
||||
|
||||
@@ -11,7 +11,8 @@ type LayingTransferSource struct {
|
||||
LayingTransferId uint `gorm:"index;not null"`
|
||||
SourceProjectFlockKandangId uint `gorm:"not null"`
|
||||
ProductWarehouseId *uint `gorm:""`
|
||||
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||
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
|
||||
Note string `gorm:"type:text"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
|
||||
@@ -10,7 +10,8 @@ type LayingTransferTarget struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
LayingTransferId uint `gorm:"index;not null"`
|
||||
TargetProjectFlockKandangId uint `gorm:"not null"`
|
||||
Qty float64 `gorm:"type:numeric(15,3);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
|
||||
ProductWarehouseId *uint `gorm:""`
|
||||
Note string `gorm:"type:text"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
|
||||
@@ -5,6 +5,7 @@ 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"`
|
||||
|
||||
@@ -7,12 +7,13 @@ 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:"-"`
|
||||
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:"-"`
|
||||
|
||||
Activities []PhaseActivity `gorm:"foreignKey:PhaseId;references:Id"`
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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"`
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package middleware
|
||||
|
||||
const(
|
||||
P_DashboardGetAll = "lti.dashboard.list"
|
||||
)
|
||||
// project-flock
|
||||
const (
|
||||
P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing"
|
||||
@@ -19,18 +22,19 @@ 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_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"
|
||||
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"
|
||||
)
|
||||
const (
|
||||
P_AdjustmentGetAll = "lti.inventory.list"
|
||||
@@ -44,7 +48,9 @@ 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 (
|
||||
@@ -134,18 +140,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_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_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_SuppliersGetAll = "lti.master.suppliers.list"
|
||||
P_SuppliersGetOne = "lti.master.suppliers.detail"
|
||||
P_SuppliersCreateOne = "lti.master.suppliers.create"
|
||||
@@ -207,15 +213,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,6 +78,36 @@ 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")
|
||||
|
||||
@@ -86,7 +116,17 @@ func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
||||
}
|
||||
|
||||
result, err := u.ClosingService.GetClosingSummary(c, uint(id))
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -108,12 +148,7 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
||||
}
|
||||
|
||||
projectFlock, err := u.ClosingService.GetProjectFlockByID(c, uint(projectFlockID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID))
|
||||
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -123,19 +158,60 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get closing penjualan successfully",
|
||||
Data: dto.ToPenjualanRealisasiResponseDTO(projectFlock.Category, uint(projectFlockID), result),
|
||||
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),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetOverhead(c *fiber.Ctx) error {
|
||||
param := c.Params("project_flock_id")
|
||||
projectParam := c.Params("project_flock_id")
|
||||
kandangParam := c.Params("project_flock_kandang_id")
|
||||
|
||||
projectFlockID, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
||||
projectFlockID, err := strconv.Atoi(projectParam)
|
||||
if err != nil || projectFlockID <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||
}
|
||||
|
||||
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID))
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -162,6 +238,14 @@ 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")
|
||||
@@ -338,7 +422,18 @@ func (u *ClosingController) GetClosingDataProduksi(c *fiber.Ctx) error {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
||||
}
|
||||
|
||||
result, err := u.ClosingService.GetClosingDataProduksi(c, uint(id))
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -59,39 +59,65 @@ 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:"average_weight"`
|
||||
AverageSellingPrice float64 `json:"chicken_average_selling_price"`
|
||||
AverageWeight float64 `json:"avg_weight"`
|
||||
AverageSellingPrice float64 `json:"avg_selling_price"`
|
||||
}
|
||||
|
||||
type ClosingEggSalesDTO struct {
|
||||
EggPieces int `json:"egg_pieces"`
|
||||
EggMassKg float64 `json:"egg_mass_kg"`
|
||||
AverageEggWeightKg float64 `json:"average_egg_weight_kg"`
|
||||
AverageSellingPrice float64 `json:"egg_average_selling_price"`
|
||||
EggMassKg float64 `json:"egg_mass"`
|
||||
AverageEggWeightKg float64 `json:"avg_egg_weight"`
|
||||
AverageSellingPrice float64 `json:"avg_selling_price"`
|
||||
}
|
||||
|
||||
type ClosingPerformanceDTO struct {
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type ClosingSalesGroupDTO struct {
|
||||
@@ -164,7 +190,7 @@ func sumPopulation(history []entity.ProjectFlockKandang) float64 {
|
||||
var total float64
|
||||
for _, h := range history {
|
||||
for _, chickin := range h.Chickins {
|
||||
total += chickin.UsageQty + chickin.PendingUsageQty
|
||||
total += chickin.UsageQty
|
||||
}
|
||||
}
|
||||
return total
|
||||
|
||||
@@ -79,10 +79,11 @@ type HppGroup struct {
|
||||
}
|
||||
|
||||
type SummaryHpp struct {
|
||||
Label string `json:"label"`
|
||||
Comparison `json:"-"`
|
||||
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
|
||||
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
|
||||
Label string `json:"label"`
|
||||
Budgeting FinancialMetrics `json:"budgeting"`
|
||||
Realization FinancialMetrics `json:"realization"`
|
||||
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
|
||||
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
|
||||
}
|
||||
|
||||
type HppPurchasesSection struct {
|
||||
@@ -246,11 +247,9 @@ func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets [
|
||||
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
||||
|
||||
summary := SummaryHpp{
|
||||
Label: label,
|
||||
Comparison: ToComparison(
|
||||
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
|
||||
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
|
||||
),
|
||||
Label: label,
|
||||
Budgeting: ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
|
||||
Realization: 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(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
|
||||
func ToPenjualanRealisasiResponseDTO(projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
|
||||
|
||||
return PenjualanRealisasiResponseDTO{
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
)
|
||||
|
||||
@@ -69,7 +71,7 @@ func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseReal
|
||||
return dto
|
||||
}
|
||||
|
||||
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64) OverheadListDTO {
|
||||
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64, isPerKandang bool, totalKandangCount int, projectFlockKandangCountMap map[uint]int) OverheadListDTO {
|
||||
overheadsByNonstockID := make(map[uint]*OverheadDTO)
|
||||
latestDateByNonstockID := make(map[uint]string)
|
||||
|
||||
@@ -82,9 +84,20 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
|
||||
itemName, itemUOM := getItemInfo(budgets[i].Nonstock)
|
||||
overheadsByNonstockID[nonstockID].ItemName = itemName
|
||||
overheadsByNonstockID[nonstockID].UOMName = itemUOM
|
||||
overheadsByNonstockID[nonstockID].BudgetQuantity = budgets[i].Qty
|
||||
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgets[i].Price
|
||||
overheadsByNonstockID[nonstockID].BudgetTotalAmount = calculateTotal(budgets[i].Qty, budgets[i].Price)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
for i := range realizations {
|
||||
@@ -97,8 +110,40 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
|
||||
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
|
||||
}
|
||||
|
||||
overheadsByNonstockID[nonstockID].ActualQuantity += realizations[i].Qty
|
||||
overheadsByNonstockID[nonstockID].ActualTotalAmount += calculateTotal(realizations[i].Qty, realizations[i].Price)
|
||||
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
|
||||
|
||||
if overheadsByNonstockID[nonstockID].ItemName == "" {
|
||||
itemName, itemUOM := getItemInfo(realizations[i].ExpenseNonstock.Nonstock)
|
||||
@@ -146,7 +191,26 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
|
||||
}
|
||||
}
|
||||
|
||||
// === Helper Functions ===
|
||||
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)
|
||||
}
|
||||
|
||||
func getItemInfo(nonstock *entity.Nonstock) (string, string) {
|
||||
if nonstock != nil && nonstock.Id != 0 {
|
||||
|
||||
@@ -11,6 +11,7 @@ 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"
|
||||
@@ -33,11 +34,13 @@ 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, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate)
|
||||
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, standardGrowthDetailRepo, productionStandardDetailRepo, validate)
|
||||
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ 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)
|
||||
@@ -166,6 +167,23 @@ 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
|
||||
|
||||
@@ -23,8 +23,10 @@ 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)
|
||||
@@ -32,4 +34,5 @@ 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,7 +2,9 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -16,6 +18,7 @@ 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"
|
||||
@@ -32,10 +35,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) ([]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)
|
||||
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)
|
||||
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)
|
||||
@@ -46,6 +49,7 @@ type closingService struct {
|
||||
Validate *validator.Validate
|
||||
Repository repository.ClosingRepository
|
||||
ProjectFlockRepo projectflockRepository.ProjectflockRepository
|
||||
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
|
||||
MarketingRepo marketingRepository.MarketingRepository
|
||||
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
@@ -54,14 +58,17 @@ 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, 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 {
|
||||
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 {
|
||||
return &closingService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
ProjectFlockRepo: projectFlockRepo,
|
||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||
MarketingRepo: marketingRepo,
|
||||
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
@@ -70,6 +77,8 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje
|
||||
ChickinRepo: chickinRepo,
|
||||
PurchaseRepo: purchaseRepo,
|
||||
RecordingRepo: recordingRepo,
|
||||
StandardGrowthDetailRepo: standardGrowthDetailRepo,
|
||||
ProductionStandardDetailRepo: productionStandardDetailRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +103,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("LOWER(flock_name) LIKE LOWER(?)", "%"+params.Search+"%")
|
||||
return db.Where("flock_name ILIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
@@ -129,24 +138,9 @@ func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.Proj
|
||||
return projectFlock, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||
func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||
|
||||
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")
|
||||
})
|
||||
realisasi, err := s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlockID, projectFlockKandangID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -154,23 +148,18 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entit
|
||||
return []entity.MarketingDeliveryProduct{}, nil
|
||||
}
|
||||
|
||||
filtered := make([]entity.MarketingDeliveryProduct, 0, len(realisasi))
|
||||
for _, item := range realisasi {
|
||||
|
||||
if item.UsageQty != 0 || item.TotalWeight != 0 || item.AvgWeight != 0 ||
|
||||
item.UnitPrice != 0 || item.TotalPrice != 0 {
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered, nil
|
||||
return realisasi, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) {
|
||||
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 kandangID != nil {
|
||||
return s.getClosingSummaryByKandang(c.Context(), projectFlockID, *kandangID)
|
||||
}
|
||||
|
||||
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")
|
||||
@@ -191,6 +180,124 @@ func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*d
|
||||
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")
|
||||
@@ -231,7 +338,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)
|
||||
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID, params.KandangID)
|
||||
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")
|
||||
@@ -311,12 +418,15 @@ func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, proje
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) {
|
||||
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint, kandangID *uint) ([]uint, error) {
|
||||
var ids []uint
|
||||
err := s.Repository.DB().WithContext(ctx).
|
||||
query := s.Repository.DB().WithContext(ctx).
|
||||
Model(&entity.ProjectFlockKandang{}).
|
||||
Where("project_flock_id = ?", projectFlockID).
|
||||
Pluck("id", &ids).Error
|
||||
Where("project_flock_id = ?", projectFlockID)
|
||||
if kandangID != nil {
|
||||
query = query.Where("kandang_id = ?", *kandangID)
|
||||
}
|
||||
err := query.Order("id ASC").Pluck("id", &ids).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -379,35 +489,90 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID
|
||||
return statusProject, statusClosing, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) {
|
||||
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error) {
|
||||
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
realizations, err := s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlockID, projectFlockKandangID)
|
||||
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
|
||||
for _, chickin := range chickins {
|
||||
totalChickinQty += chickin.UsageQty
|
||||
}
|
||||
var totalDepletion float64
|
||||
|
||||
totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
totalActualPopulation := totalChickinQty - totalDepletion
|
||||
|
||||
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation)
|
||||
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation, projectFlockKandangID != nil, totalKandangCount, projectFlockKandangCountMap)
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
@@ -520,12 +685,22 @@ func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, proj
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error) {
|
||||
func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error) {
|
||||
if projectFlockID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||
}
|
||||
|
||||
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations)
|
||||
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)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
|
||||
}
|
||||
@@ -534,19 +709,29 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||
}
|
||||
|
||||
var population float64
|
||||
for _, history := range project.KandangHistory {
|
||||
for _, chickin := range history.Chickins {
|
||||
population += chickin.UsageQty
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
isGrowing := strings.EqualFold(project.Category, string(utils.ProjectFlockCategoryGrowing))
|
||||
|
||||
projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
|
||||
currentWeek, err := s.determineProductionWeek(c.Context(), projectFlockKandangIDs)
|
||||
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")
|
||||
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
|
||||
}
|
||||
|
||||
feedIn, feedUsed, err := s.Repository.SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
|
||||
@@ -555,6 +740,40 @@ 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)
|
||||
@@ -577,10 +796,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),
|
||||
@@ -588,7 +807,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)}
|
||||
@@ -621,6 +840,9 @@ 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
|
||||
@@ -668,6 +890,9 @@ 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
|
||||
}
|
||||
|
||||
@@ -684,15 +909,63 @@ 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.Awg = eggPerformance.Awg
|
||||
// performance.DeffFcr = eggPerformance.DeffFcr
|
||||
performance.AwgAct = eggPerformance.AwgAct
|
||||
} else {
|
||||
performance.FcrStd = chickenPerformance.FcrStd
|
||||
// performance.FcrStd = chickenPerformance.FcrStd
|
||||
performance.FcrAct = chickenPerformance.FcrAct
|
||||
performance.DeffFcr = chickenPerformance.DeffFcr
|
||||
performance.Awg = chickenPerformance.Awg
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := dto.ClosingProductionReportDTO{
|
||||
@@ -738,6 +1011,46 @@ 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)
|
||||
|
||||
@@ -768,7 +1081,7 @@ func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopul
|
||||
FcrStd: fcrStd,
|
||||
FcrAct: fcrAct,
|
||||
DeffFcr: deffFcr,
|
||||
Awg: awg,
|
||||
AwgAct: awg,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ 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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ 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,
|
||||
@@ -150,6 +151,10 @@ 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,12 +308,22 @@ 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),
|
||||
Data: dto.ToDailyChecklistDetailDTO(detail.Checklist, detail.Phases, detail.Tasks, detail.AssignedEmployees, detail.TotalActivities, detail.Progress, documentDTOs),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -342,6 +357,12 @@ 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,6 +31,7 @@ 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 {
|
||||
@@ -40,6 +41,14 @@ 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 {
|
||||
@@ -55,11 +64,12 @@ type DailyChecklistSummaryDTO struct {
|
||||
}
|
||||
|
||||
type DailyChecklistPerformanceOverviewDTO struct {
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type DailyChecklistReportDTO struct {
|
||||
@@ -165,10 +175,11 @@ 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) DailyChecklistDetailDTO {
|
||||
func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity.DailyChecklistPhase, tasks []entity.DailyChecklistActivityTask, assignedEmployees []entity.Employee, totalActivities int, progress float64, documentURLs []DailyChecklistDocumentDTO) DailyChecklistDetailDTO {
|
||||
phaseDTOs := make([]DailyChecklistPhaseDTO, 0, len(phases))
|
||||
for _, phase := range phases {
|
||||
phaseDTOs = append(phaseDTOs, DailyChecklistPhaseDTO{
|
||||
@@ -228,5 +239,6 @@ func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity.
|
||||
AssignedEmployees: assignedDTOs,
|
||||
TotalActivity: totalActivities,
|
||||
Progress: progress,
|
||||
DocumentURLs: documentURLs,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
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"
|
||||
@@ -19,8 +24,13 @@ 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)
|
||||
dailyChecklistService := sDailyChecklist.NewDailyChecklistService(dailyChecklistRepo, phasesRepo, validate, documentSvc)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
DailyChecklistRoutes(router, userService, dailyChecklistService)
|
||||
|
||||
@@ -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)
|
||||
|
||||
// create daily checklist
|
||||
// upsert daily checklist
|
||||
route.Post("/", ctrl.CreateOne)
|
||||
|
||||
// get detail data daily checklist by id
|
||||
|
||||
@@ -9,6 +9,7 @@ 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"
|
||||
@@ -17,6 +18,7 @@ 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"
|
||||
)
|
||||
@@ -39,10 +41,18 @@ type DailyChecklistService interface {
|
||||
}
|
||||
|
||||
type dailyChecklistService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
Repository repository.DailyChecklistRepository
|
||||
PhaseRepo phaseRepo.PhasesRepository
|
||||
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
|
||||
}
|
||||
|
||||
type DailyChecklistDetail struct {
|
||||
@@ -52,6 +62,7 @@ type DailyChecklistDetail struct {
|
||||
AssignedEmployees []entity.Employee
|
||||
TotalActivities int
|
||||
Progress float64
|
||||
DocumentURLs []DailyChecklistDocument
|
||||
}
|
||||
|
||||
type DailyChecklistListItem struct {
|
||||
@@ -60,6 +71,7 @@ type DailyChecklistListItem struct {
|
||||
Date time.Time
|
||||
Category string
|
||||
Status *string
|
||||
RejectReason *string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Kandang entity.Kandang
|
||||
@@ -108,12 +120,13 @@ type DailyChecklistReportCategory struct {
|
||||
Baik int
|
||||
}
|
||||
|
||||
func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate) DailyChecklistService {
|
||||
func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate, documentSvc commonSvc.DocumentService) DailyChecklistService {
|
||||
return &dailyChecklistService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
PhaseRepo: phaseRepo,
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
PhaseRepo: phaseRepo,
|
||||
DocumentSvc: documentSvc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +171,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 ILIKE ?)", like, like)
|
||||
db = db.Where("(k.name ILIKE ? OR dc.category::text ILIKE ?)", like, like)
|
||||
}
|
||||
|
||||
countDB := db.Session(&gorm.Session{})
|
||||
@@ -174,6 +187,7 @@ 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
|
||||
@@ -192,6 +206,7 @@ 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,
|
||||
@@ -265,6 +280,7 @@ 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],
|
||||
@@ -345,6 +361,29 @@ 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,
|
||||
@@ -352,6 +391,7 @@ func (s dailyChecklistService) GetDetail(c *fiber.Ctx, id uint) (*DailyChecklist
|
||||
AssignedEmployees: assignedEmployees,
|
||||
TotalActivities: totalActivities,
|
||||
Progress: progress,
|
||||
DocumentURLs: documentURLs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -377,7 +417,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{"status": status, "updated_at": time.Now()}),
|
||||
DoUpdates: clause.Assignments(map[string]any{"updated_at": time.Now()}),
|
||||
}).Create(createBody).Error
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to upsert dailyChecklist: %+v", err)
|
||||
@@ -392,6 +432,22 @@ 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,
|
||||
}
|
||||
@@ -400,6 +456,40 @@ 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")
|
||||
@@ -869,7 +959,8 @@ 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("EXTRACT(YEAR FROM dc.date) = ?", params.Year).
|
||||
Where("dc.status = ?", "APPROVED")
|
||||
|
||||
if params.AreaID != nil {
|
||||
db = db.Where("a.id = ?", *params.AreaID)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
)
|
||||
|
||||
type Create struct {
|
||||
Date string `json:"date" validate:"required"`
|
||||
KandangId uint `json:"kandang_id" validate:"required"`
|
||||
@@ -8,8 +12,10 @@ type Create struct {
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Status string `json:"status" validate:"required"`
|
||||
RejectReason *string `json:"reject_reason"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,725 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
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
@@ -0,0 +1,54 @@
|
||||
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,10 +229,12 @@ func (u *ExpenseController) Approval(c *fiber.Ctx) error {
|
||||
|
||||
path := c.Path()
|
||||
approvalType := ""
|
||||
if strings.Contains(path, "/approvals/manager") {
|
||||
approvalType = "manager"
|
||||
if strings.Contains(path, "/approvals/head-area") {
|
||||
approvalType = "head-area"
|
||||
} 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,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -15,6 +16,7 @@ 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)
|
||||
}
|
||||
|
||||
@@ -55,6 +57,40 @@ 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
|
||||
@@ -75,7 +111,7 @@ func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context
|
||||
Joins("LEFT JOIN suppliers ON suppliers.id = expenses.supplier_id")
|
||||
|
||||
if filters.Search != "" {
|
||||
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(?)",
|
||||
db = db.Where("expenses.category ILIKE ? OR expenses.reference_number ILIKE ? OR expenses.po_number ILIKE ? OR expenses.notes ILIKE ? OR suppliers.name ILIKE ?",
|
||||
"%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%")
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,11 @@ 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/manager", m.RequirePermissions(m.P_ExpenseApprovalManager), ctrl.Approval)
|
||||
|
||||
route.Post("/approvals/head-area", m.RequirePermissions(m.P_ExpenseApprovalHeadArea), 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("LOWER(category) LIKE LOWER(?)", "%"+params.Search+"%")
|
||||
return db.Where("category ILIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
@@ -1049,21 +1049,30 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
|
||||
}
|
||||
|
||||
var stepNumber approvalutils.ApprovalStep
|
||||
if approvalType == "manager" {
|
||||
if approvalType == "head-area" {
|
||||
|
||||
stepNumber = utils.ExpenseStepManager
|
||||
stepNumber = utils.ExpenseStepHeadArea
|
||||
if latestApproval.StepNumber != uint16(utils.ExpenseStepPengajuan) {
|
||||
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
|
||||
return fiber.NewError(fiber.StatusBadRequest,
|
||||
fmt.Sprintf("Cannot process at Manager step. Latest approval is at %s step. Expected previous step: Pengajuan", currentStepName))
|
||||
fmt.Sprintf("Cannot process at Head Area 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.ExpenseStepManager) {
|
||||
if latestApproval.StepNumber != uint16(utils.ExpenseStepUnitVicePresident) {
|
||||
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: Manager", currentStepName))
|
||||
fmt.Sprintf("Cannot process at Finance step. Latest approval is at %s step. Expected previous step: Unit Vice President", currentStepName))
|
||||
}
|
||||
} else {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid approval type: %v", approvalType))
|
||||
|
||||
+2
-2
@@ -399,11 +399,11 @@ func (r *ProductWarehouseRepositoryImpl) ListProductIDsByFlagPrefixes(ctx contex
|
||||
}
|
||||
like := prefix + "%"
|
||||
if !applied {
|
||||
db = db.Where("LOWER(flags.name) LIKE LOWER(?)", like)
|
||||
db = db.Where("flags.name ILIKE ?", like)
|
||||
applied = true
|
||||
continue
|
||||
}
|
||||
db = db.Or("LOWER(flags.name) LIKE LOWER(?)", like)
|
||||
db = db.Or("flags.name ILIKE ?", 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("LOWER(movement_number) LIKE LOWER(?)", "%"+strings.TrimSpace(params.Search)+"%")
|
||||
db = db.Where("movement_number ILIKE ?", "%"+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,7 +84,6 @@ 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)
|
||||
@@ -92,7 +91,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{}).
|
||||
@@ -106,13 +105,11 @@ 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
|
||||
@@ -122,7 +119,6 @@ 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
|
||||
@@ -220,7 +216,6 @@ 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
|
||||
@@ -228,7 +223,7 @@ func (b *transferExpenseBridge) createExpenseViaService(
|
||||
|
||||
costItems = append(costItems, expenseValidation.CostItem{
|
||||
NonstockID: expeditionNonstockID,
|
||||
Quantity: 1,
|
||||
Quantity: 1,
|
||||
Price: price,
|
||||
Notes: note,
|
||||
})
|
||||
@@ -251,14 +246,16 @@ 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.ExpenseStepManager, &action, actorID, nil); err != nil {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||
@@ -328,7 +325,6 @@ 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").
|
||||
@@ -348,11 +344,10 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -395,17 +390,14 @@ 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,6 +14,7 @@ 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)
|
||||
@@ -53,6 +54,43 @@ 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
|
||||
|
||||
@@ -99,13 +137,14 @@ 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")
|
||||
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id").
|
||||
Where("marketing_delivery_products.delivery_date IS NOT NULL")
|
||||
|
||||
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" {
|
||||
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" || filters.MarketingType != "" {
|
||||
db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id")
|
||||
}
|
||||
|
||||
if filters.ProductId > 0 || filters.Search != "" {
|
||||
if filters.ProductId > 0 || filters.Search != "" || filters.MarketingType != "" {
|
||||
db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id")
|
||||
}
|
||||
|
||||
@@ -139,6 +178,29 @@ 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("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
|
||||
return db.Where("name ILIKE ?", "%"+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("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
|
||||
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
|
||||
@@ -3,14 +3,13 @@ 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"
|
||||
@@ -54,7 +53,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("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
|
||||
return db.Where("name ILIKE ?", "%"+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("LOWER(employees.name) LIKE LOWER(?)", "%"+params.Search+"%")
|
||||
db = db.Where("employees.name ILIKE ?", "%"+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("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
|
||||
return db.Where("name ILIKE ?", "%"+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("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
|
||||
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
|
||||
@@ -3,14 +3,13 @@ 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"
|
||||
@@ -55,7 +54,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("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
|
||||
return db.Where("name ILIKE ?", "%"+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=100"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=500"`
|
||||
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("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
|
||||
db = db.Where("name ILIKE ?", "%"+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=100"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=500"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
AreaId int `query:"area_id" validate:"omitempty,number,gt=0"`
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ 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"
|
||||
)
|
||||
@@ -23,7 +22,7 @@ type NonstockListDTO struct {
|
||||
Name string `json:"name"`
|
||||
Flags []string `json:"flags"`
|
||||
Uom *uomDTO.UomRelationDTO `json:"uom"`
|
||||
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
|
||||
Suppliers []NonstockSupplierDTO `json:"suppliers"`
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
@@ -33,6 +32,14 @@ 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 {
|
||||
@@ -99,21 +106,27 @@ func ToNonstockDetailDTO(e entity.Nonstock) NonstockDetailDTO {
|
||||
}
|
||||
}
|
||||
|
||||
func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []supplierDTO.SupplierRelationDTO {
|
||||
func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []NonstockSupplierDTO {
|
||||
if len(relations) == 0 {
|
||||
return make([]supplierDTO.SupplierRelationDTO, 0)
|
||||
return make([]NonstockSupplierDTO, 0)
|
||||
}
|
||||
|
||||
result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations))
|
||||
result := make([]NonstockSupplierDTO, 0, len(relations))
|
||||
for _, relation := range relations {
|
||||
if relation.Supplier.Id == 0 {
|
||||
continue
|
||||
}
|
||||
result = append(result, supplierDTO.ToSupplierRelationDTO(relation.Supplier))
|
||||
result = append(result, NonstockSupplierDTO{
|
||||
Id: relation.Supplier.Id,
|
||||
Name: relation.Supplier.Name,
|
||||
Alias: relation.Supplier.Alias,
|
||||
Category: relation.Supplier.Category,
|
||||
Price: relation.Price,
|
||||
})
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
return make([]supplierDTO.SupplierRelationDTO, 0)
|
||||
return make([]NonstockSupplierDTO, 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, supplierIDs []uint) error
|
||||
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, suppliers []entity.NonstockSupplier) 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, supplierIDs []uint) error {
|
||||
func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, suppliers []entity.NonstockSupplier) error {
|
||||
db := tx
|
||||
if db == nil {
|
||||
db = r.DB()
|
||||
}
|
||||
|
||||
if supplierIDs == nil {
|
||||
if suppliers == nil {
|
||||
return db.WithContext(ctx).
|
||||
Where("nonstock_id = ?", nonstockID).
|
||||
Delete(&entity.NonstockSupplier{}).
|
||||
@@ -61,18 +61,31 @@ func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm
|
||||
return err
|
||||
}
|
||||
|
||||
existingMap := make(map[uint]struct{}, len(existing))
|
||||
existingMap := make(map[uint]entity.NonstockSupplier, len(existing))
|
||||
for _, rel := range existing {
|
||||
existingMap[rel.SupplierId] = struct{}{}
|
||||
existingMap[rel.SupplierId] = rel
|
||||
}
|
||||
|
||||
incomingMap := make(map[uint]struct{}, len(supplierIDs))
|
||||
for _, id := range supplierIDs {
|
||||
incomingMap[id] = struct{}{}
|
||||
if _, exists := existingMap[id]; exists {
|
||||
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
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
record := entity.NonstockSupplier{NonstockId: nonstockID, SupplierId: id}
|
||||
record := entity.NonstockSupplier{
|
||||
NonstockId: nonstockID,
|
||||
SupplierId: rel.SupplierId,
|
||||
Price: rel.Price,
|
||||
}
|
||||
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("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
|
||||
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
@@ -111,8 +111,25 @@ func (s *nonstockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
|
||||
return nil, err
|
||||
}
|
||||
|
||||
supplierIDs := utils.UniqueUintSlice(req.SupplierIDs)
|
||||
if len(supplierIDs) > 0 {
|
||||
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,
|
||||
})
|
||||
}
|
||||
supplierList, supplierErr := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
|
||||
if supplierErr != nil {
|
||||
s.Log.Errorf("Failed to validate suppliers: %+v", supplierErr)
|
||||
@@ -155,7 +172,7 @@ func (s *nonstockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
|
||||
return err
|
||||
}
|
||||
|
||||
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierIDs)
|
||||
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierLinks)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -193,15 +210,27 @@ func (s nonstockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint
|
||||
updateBody["uom_id"] = *req.UomID
|
||||
}
|
||||
|
||||
var supplierIDs []uint
|
||||
var supplierLinks []entity.NonstockSupplier
|
||||
var supplierUpdate bool
|
||||
if req.SupplierIDs != nil {
|
||||
if req.Suppliers != nil {
|
||||
supplierUpdate = true
|
||||
supplierIDs = utils.UniqueUintSlice(*req.SupplierIDs)
|
||||
if len(supplierIDs) > 0 {
|
||||
var supplierList []entity.Supplier
|
||||
var supplierErr error
|
||||
supplierList, supplierErr = s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
|
||||
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)
|
||||
if supplierErr != nil {
|
||||
s.Log.Errorf("Failed to validate suppliers: %+v", supplierErr)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers")
|
||||
@@ -253,11 +282,7 @@ func (s nonstockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint
|
||||
}
|
||||
|
||||
if supplierUpdate {
|
||||
var ids []uint
|
||||
if len(supplierIDs) > 0 {
|
||||
ids = supplierIDs
|
||||
}
|
||||
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, ids); err != nil {
|
||||
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, supplierLinks); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
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"`
|
||||
SupplierIDs []uint `json:"supplier_ids" validate:"dive,gt=0"`
|
||||
Suppliers []SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
|
||||
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"`
|
||||
SupplierIDs *[]uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
|
||||
Suppliers *[]SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
|
||||
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("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
|
||||
db = db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
if params.PhaseIDs != "" {
|
||||
ids := parseIDs(params.PhaseIDs)
|
||||
|
||||
@@ -15,12 +15,13 @@ type PhasesRelationDTO struct {
|
||||
}
|
||||
|
||||
type PhasesListDTO struct {
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type PhasesDetailDTO struct {
|
||||
@@ -44,12 +45,13 @@ func ToPhasesListDTO(e entity.Phases) PhasesListDTO {
|
||||
// }
|
||||
|
||||
return PhasesListDTO{
|
||||
Id: e.Id,
|
||||
Name: e.Name,
|
||||
Category: e.Category,
|
||||
IsActive: e.IsActive,
|
||||
CreatedAt: e.CreatedAt,
|
||||
CreatedUser: createdUser,
|
||||
Id: e.Id,
|
||||
Name: e.Name,
|
||||
Category: e.Category,
|
||||
IsActive: e.IsActive,
|
||||
ActivityCount: e.ActivityCount,
|
||||
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("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
|
||||
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
if params.Category != nil {
|
||||
db = db.Where("category = ?", *params.Category)
|
||||
@@ -63,6 +63,40 @@ 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("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
|
||||
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
|
||||
+82
-1
@@ -1,8 +1,10 @@
|
||||
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"
|
||||
@@ -22,6 +24,8 @@ 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 {
|
||||
@@ -63,7 +67,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("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
|
||||
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
if params.ProjectCategory != "" {
|
||||
return db.Where("project_category = ?", params.ProjectCategory)
|
||||
@@ -299,3 +303,80 @@ 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,7 +5,6 @@ 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"
|
||||
)
|
||||
@@ -20,7 +19,7 @@ type ProductRelationDTO struct {
|
||||
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
|
||||
Flags *[]string `json:"flags,omitempty"`
|
||||
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
|
||||
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
|
||||
Suppliers []ProductSupplierDTO `json:"suppliers"`
|
||||
}
|
||||
|
||||
type ProductListDTO struct {
|
||||
@@ -35,7 +34,7 @@ type ProductListDTO struct {
|
||||
Flags []string `json:"flags"`
|
||||
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
|
||||
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
|
||||
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
|
||||
Suppliers []ProductSupplierDTO `json:"suppliers"`
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
@@ -45,6 +44,14 @@ 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 {
|
||||
@@ -134,21 +141,27 @@ func ToProductDetailDTO(e entity.Product) ProductDetailDTO {
|
||||
}
|
||||
}
|
||||
|
||||
func toProductSupplierDTOs(relations []entity.ProductSupplier) []supplierDTO.SupplierRelationDTO {
|
||||
func toProductSupplierDTOs(relations []entity.ProductSupplier) []ProductSupplierDTO {
|
||||
if len(relations) == 0 {
|
||||
return make([]supplierDTO.SupplierRelationDTO, 0)
|
||||
return make([]ProductSupplierDTO, 0)
|
||||
}
|
||||
|
||||
result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations))
|
||||
result := make([]ProductSupplierDTO, 0, len(relations))
|
||||
for _, relation := range relations {
|
||||
if relation.Supplier.Id == 0 {
|
||||
continue
|
||||
}
|
||||
result = append(result, supplierDTO.ToSupplierRelationDTO(relation.Supplier))
|
||||
result = append(result, ProductSupplierDTO{
|
||||
Id: relation.Supplier.Id,
|
||||
Name: relation.Supplier.Name,
|
||||
Alias: relation.Supplier.Alias,
|
||||
Category: relation.Supplier.Category,
|
||||
Price: relation.Price,
|
||||
})
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
return make([]supplierDTO.SupplierRelationDTO, 0)
|
||||
return make([]ProductSupplierDTO, 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, supplierIDs []uint) error
|
||||
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, suppliers []entity.ProductSupplier) 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, supplierIds []uint) error {
|
||||
func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, suppliers []entity.ProductSupplier) error {
|
||||
db := tx
|
||||
if db == nil {
|
||||
db = r.DB()
|
||||
}
|
||||
|
||||
if supplierIds == nil {
|
||||
if suppliers == nil {
|
||||
return db.WithContext(ctx).
|
||||
Where("product_id = ?", productID).
|
||||
Delete(&entity.ProductSupplier{}).
|
||||
@@ -123,18 +123,31 @@ func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.
|
||||
return err
|
||||
}
|
||||
|
||||
existingMap := make(map[uint]struct{}, len(existing))
|
||||
existingMap := make(map[uint]entity.ProductSupplier, len(existing))
|
||||
for _, rel := range existing {
|
||||
existingMap[rel.SupplierId] = struct{}{}
|
||||
existingMap[rel.SupplierId] = rel
|
||||
}
|
||||
|
||||
incomingMap := make(map[uint]struct{}, len(supplierIds))
|
||||
for _, id := range supplierIds {
|
||||
incomingMap[id] = struct{}{}
|
||||
if _, exists := existingMap[id]; exists {
|
||||
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
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
record := entity.ProductSupplier{ProductId: productID, SupplierId: id}
|
||||
record := entity.ProductSupplier{
|
||||
ProductId: productID,
|
||||
SupplierId: rel.SupplierId,
|
||||
Price: rel.Price,
|
||||
}
|
||||
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("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
|
||||
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
if params.ProductCategoryID != 0 {
|
||||
return db.Where("product_category_id = ?", params.ProductCategoryID)
|
||||
@@ -138,9 +138,25 @@ func (s *productService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
||||
return nil, err
|
||||
}
|
||||
|
||||
supplierIDs := utils.UniqueUintSlice(req.SupplierIDs)
|
||||
var err error
|
||||
if len(supplierIDs) > 0 {
|
||||
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,
|
||||
})
|
||||
}
|
||||
suppliers, err := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to validate suppliers: %+v", err)
|
||||
@@ -176,10 +192,11 @@ 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 {
|
||||
@@ -190,7 +207,7 @@ func (s *productService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
||||
return err
|
||||
}
|
||||
|
||||
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierIDs)
|
||||
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierLinks)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -275,15 +292,27 @@ func (s productService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
|
||||
ctx := c.Context()
|
||||
|
||||
var suppliers []entity.Supplier
|
||||
var supplierIDs []uint
|
||||
var supplierLinks []entity.ProductSupplier
|
||||
var supplierUpdate bool
|
||||
if req.SupplierIDs != nil {
|
||||
if req.Suppliers != nil {
|
||||
supplierUpdate = true
|
||||
supplierIDs = utils.UniqueUintSlice(*req.SupplierIDs)
|
||||
if len(supplierIDs) > 0 {
|
||||
var err error
|
||||
suppliers, err = s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
|
||||
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)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to validate suppliers: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers")
|
||||
@@ -335,11 +364,7 @@ func (s productService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
}
|
||||
|
||||
if supplierUpdate {
|
||||
var ids []uint
|
||||
if len(supplierIDs) > 0 {
|
||||
ids = supplierIDs
|
||||
}
|
||||
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, ids); err != nil {
|
||||
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, supplierLinks); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
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"`
|
||||
@@ -10,7 +15,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"`
|
||||
SupplierIDs []uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
|
||||
Suppliers []SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
|
||||
Flags []string `json:"flags,omitempty" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
@@ -24,7 +29,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"`
|
||||
SupplierIDs *[]uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
|
||||
Suppliers *[]SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
|
||||
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ 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"`
|
||||
}
|
||||
@@ -42,6 +43,7 @@ func toSupplierNonstockDTOs(relations []entity.NonstockSupplier) []SupplierNonst
|
||||
result = append(result, SupplierNonstockDTO{
|
||||
Id: Nonstock.Id,
|
||||
Name: Nonstock.Name,
|
||||
Price: relation.Price,
|
||||
Uom: uomRef,
|
||||
Flags: flags,
|
||||
})
|
||||
|
||||
@@ -8,12 +8,13 @@ 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)"`
|
||||
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)"`
|
||||
SupplierPrice float64 `json:"supplier_price"`
|
||||
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
|
||||
Flags []string `json:"flags"`
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
@@ -42,12 +43,13 @@ func toSupplierProductDTOs(relations []entity.ProductSupplier) []SupplierProduct
|
||||
}
|
||||
|
||||
result = append(result, SupplierProductDTO{
|
||||
Id: product.Id,
|
||||
Name: product.Name,
|
||||
ProductPrice: product.ProductPrice,
|
||||
SellingPrice: product.SellingPrice,
|
||||
Uom: uomRef,
|
||||
Flags: flags,
|
||||
Id: product.Id,
|
||||
Name: product.Name,
|
||||
ProductPrice: product.ProductPrice,
|
||||
SellingPrice: product.SellingPrice,
|
||||
SupplierPrice: relation.Price,
|
||||
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("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
|
||||
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
|
||||
if params.Category != "" {
|
||||
db = db.Where("LOWER(category) LIKE LOWER(?)", "%"+params.Category+"%")
|
||||
db = db.Where("category ILIKE ?", "%"+params.Category+"%")
|
||||
}
|
||||
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
|
||||
@@ -15,15 +15,9 @@ func UomRoutes(v1 fiber.Router, u user.UserService, s uom.UomService) {
|
||||
route := v1.Group("/uoms")
|
||||
route.Use(m.Auth(u))
|
||||
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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("LOWER(name) LIKE LOWER(?)", "%"+params.Search+"%")
|
||||
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
|
||||
@@ -3,14 +3,13 @@ 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"
|
||||
@@ -54,7 +53,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("LOWER(warehouses.name) LIKE LOWER(?)", "%"+params.Search+"%")
|
||||
db = db.Where("warehouses.name ILIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
if params.AreaId != 0 {
|
||||
db = db.Where("area_id = ?", params.AreaId)
|
||||
|
||||
@@ -14,6 +14,7 @@ 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"
|
||||
@@ -38,6 +39,7 @@ 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)
|
||||
@@ -88,6 +90,7 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
||||
kandangRepo,
|
||||
warehouseRepo,
|
||||
productWarehouseRepo,
|
||||
productRepo,
|
||||
projectFlockRepo,
|
||||
projectflockkandangrepo,
|
||||
projectflockpopulationrepo,
|
||||
|
||||
@@ -12,6 +12,7 @@ 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"
|
||||
@@ -44,6 +45,7 @@ type chickinService struct {
|
||||
KandangRepo KandangRepo.KandangRepository
|
||||
WarehouseRepo rWarehouse.WarehouseRepository
|
||||
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||
ProductRepo rProduct.ProductRepository
|
||||
ProjectFlockRepo rProjectFlock.ProjectflockRepository
|
||||
ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository
|
||||
ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
|
||||
@@ -52,7 +54,7 @@ type chickinService struct {
|
||||
StockLogRepo rStockLogs.StockLogRepository
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return &chickinService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
@@ -60,6 +62,7 @@ func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo Kan
|
||||
KandangRepo: kandangRepo,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
ProductRepo: productRepo,
|
||||
ProjectFlockRepo: projectFlockRepo,
|
||||
ProjectflockKandangRepo: projectflockkandangRepo,
|
||||
ProjectflockPopulationRepo: projectflockpopulationRepo,
|
||||
@@ -99,7 +102,6 @@ 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
|
||||
@@ -347,7 +349,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -380,7 +381,6 @@ 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,6 +449,7 @@ 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(
|
||||
@@ -479,39 +480,55 @@ 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) {
|
||||
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")
|
||||
}
|
||||
targetFlag = utils.FlagPullet
|
||||
} else if category == string(utils.ProjectFlockCategoryLaying) {
|
||||
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId)
|
||||
targetFlag = utils.FlagLayer
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, chickin := range chickins {
|
||||
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(c.Context(), chickin.Id)
|
||||
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")
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to check population for chickin %d", chickin.Id))
|
||||
}
|
||||
if populationExists {
|
||||
continue
|
||||
}
|
||||
|
||||
pfkID := approvableID
|
||||
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "LAYER", dbTransaction, actorID, &pfkID)
|
||||
sourcePW, err := s.ProductWarehouseRepo.GetByID(c.Context(), chickin.ProductWarehouseId, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Product.Flags")
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create LAYER product warehouse")
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get product warehouse 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")
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -534,7 +551,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -568,104 +584,35 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint, projectFlockKandangId *uint) (*entity.ProductWarehouse, error) {
|
||||
|
||||
products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(ctx.Context(), categoryCode, warehouseId)
|
||||
if err == nil && len(products) > 0 {
|
||||
existingPW := &products[0]
|
||||
|
||||
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
|
||||
// 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
|
||||
}
|
||||
|
||||
product, err := s.ProductWarehouseRepo.GetFirstProductByFlag(ctx.Context(), categoryCode)
|
||||
currentFlags, err := s.ProductRepo.GetFlags(ctx, productID)
|
||||
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)
|
||||
return fmt.Errorf("failed to get product flags: %w", err)
|
||||
}
|
||||
|
||||
newPW := &entity.ProductWarehouse{
|
||||
ProductId: product.Id,
|
||||
WarehouseId: warehouseId,
|
||||
ProjectFlockKandangId: projectFlockKandangId,
|
||||
Quantity: 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 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)
|
||||
if hasTargetFlag {
|
||||
return nil
|
||||
}
|
||||
|
||||
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")
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -674,9 +621,6 @@ 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,
|
||||
@@ -686,13 +630,9 @@ 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
|
||||
}
|
||||
@@ -706,10 +646,7 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
|
||||
CreatedBy: actorID,
|
||||
Notes: fmt.Sprintf("Chickin #%d", chickin.Id),
|
||||
}
|
||||
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)
|
||||
|
||||
}
|
||||
s.StockLogRepo.CreateOne(ctx, decreaseLog, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -720,93 +657,17 @@ func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB
|
||||
return nil
|
||||
}
|
||||
|
||||
sourcePW, err := s.ProductWarehouseRepo.GetByID(ctx, chickin.ProductWarehouseId, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Product.Flags")
|
||||
_, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
||||
StockableKey: fifo.StockableKeyProjectFlockPopulation,
|
||||
StockableID: population.Id,
|
||||
ProductWarehouseID: targetPW.Id,
|
||||
Quantity: chickin.UsageQty,
|
||||
Tx: tx,
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
@@ -825,7 +686,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -842,9 +702,7 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
||||
CreatedBy: actorID,
|
||||
Notes: fmt.Sprintf("Chickin #%d - Stock released", chickin.Id),
|
||||
}
|
||||
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)
|
||||
}
|
||||
s.StockLogRepo.CreateOne(ctx, increaseLog, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+3
-3
@@ -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), 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).
|
||||
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).
|
||||
Scan(&total).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
||||
@@ -14,44 +14,83 @@ 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"`
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type RecordingListDTO struct {
|
||||
RecordingRelationDTO
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type RecordingDetailDTO struct {
|
||||
RecordingListDTO
|
||||
Depletions []RecordingDepletionDTO `json:"depletions"`
|
||||
Stocks []RecordingStockDTO `json:"stocks"`
|
||||
Eggs []RecordingEggDTO `json:"eggs"`
|
||||
Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"`
|
||||
ProductCategory string `json:"product_category"`
|
||||
Depletions []RecordingDepletionDTO `json:"depletions"`
|
||||
Stocks []RecordingStockDTO `json:"stocks"`
|
||||
Eggs []RecordingEggDTO `json:"eggs"`
|
||||
}
|
||||
|
||||
type RecordingDepletionDTO struct {
|
||||
@@ -63,7 +102,7 @@ type RecordingDepletionDTO struct {
|
||||
type RecordingStockDTO struct {
|
||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||
UsageAmount float64 `json:"usage_amount"`
|
||||
PendingQty *float64 `json:"pending_qty,omitempty"`
|
||||
PendingQty float64 `json:"pending_qty"`
|
||||
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
|
||||
}
|
||||
|
||||
@@ -75,117 +114,10 @@ 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 {
|
||||
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,
|
||||
}
|
||||
return toRecordingListDTO(e)
|
||||
}
|
||||
|
||||
func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO {
|
||||
@@ -197,20 +129,15 @@ func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO {
|
||||
}
|
||||
|
||||
func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
|
||||
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)
|
||||
}
|
||||
listDTO := toRecordingListDTO(e)
|
||||
|
||||
return RecordingDetailDTO{
|
||||
RecordingListDTO: listDTO,
|
||||
Depletions: ToRecordingDepletionDTOs(e.Depletions),
|
||||
Stocks: ToRecordingStockDTOs(e.Stocks),
|
||||
Eggs: eggs,
|
||||
Warehouse: recordingWarehouseDTO(e),
|
||||
ProductCategory: recordingProductCategory(e),
|
||||
Depletions: ToRecordingDepletionDTOs(e.Depletions),
|
||||
Stocks: ToRecordingStockDTOs(e.Stocks),
|
||||
Eggs: ToRecordingEggDTOs(e.Eggs),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,11 +160,15 @@ 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: s.PendingQty,
|
||||
PendingQty: pendingQty,
|
||||
ProductWarehouse: mapProductWarehouseDTO(&s.ProductWarehouse),
|
||||
}
|
||||
}
|
||||
@@ -258,6 +189,184 @@ 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{}
|
||||
@@ -271,6 +380,20 @@ 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,6 +11,8 @@ 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"
|
||||
@@ -29,6 +31,16 @@ 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{
|
||||
@@ -63,6 +75,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
approvalRepo,
|
||||
approvalService,
|
||||
fifoService,
|
||||
productionStandardService,
|
||||
validate,
|
||||
)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
@@ -48,12 +48,30 @@ 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),
|
||||
@@ -64,19 +82,28 @@ 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").
|
||||
Preload("Eggs.ProductWarehouse.Warehouse.Area").
|
||||
Preload("Eggs.ProductWarehouse.Warehouse.Location")
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) {
|
||||
@@ -433,6 +460,67 @@ 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,6 +10,7 @@ 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"
|
||||
@@ -53,6 +54,7 @@ type recordingService struct {
|
||||
ProjectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
|
||||
ApprovalRepo commonRepo.ApprovalRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
ProductionStandardSvc sProductionStandard.ProductionStandardService
|
||||
FifoSvc commonSvc.FifoService
|
||||
}
|
||||
|
||||
@@ -64,6 +66,7 @@ func NewRecordingService(
|
||||
approvalRepo commonRepo.ApprovalRepository,
|
||||
approvalSvc commonSvc.ApprovalService,
|
||||
fifoSvc commonSvc.FifoService,
|
||||
productionStandardSvc sProductionStandard.ProductionStandardService,
|
||||
validate *validator.Validate,
|
||||
) RecordingService {
|
||||
return &recordingService{
|
||||
@@ -75,6 +78,7 @@ func NewRecordingService(
|
||||
ProjectFlockPopulationRepo: projectFlockPopulationRepo,
|
||||
ApprovalRepo: approvalRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
ProductionStandardSvc: productionStandardSvc,
|
||||
FifoSvc: fifoSvc,
|
||||
}
|
||||
}
|
||||
@@ -169,6 +173,14 @@ 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 {
|
||||
@@ -188,6 +200,11 @@ 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")
|
||||
@@ -210,8 +227,12 @@ 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)
|
||||
@@ -1157,7 +1178,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
||||
|
||||
var fcrValue float64
|
||||
if usageInGrams > 0 && totalEggWeightGrams > 0 {
|
||||
fcrValue = totalEggWeightGrams / usageInGrams
|
||||
fcrValue = usageInGrams / totalEggWeightGrams
|
||||
updates["fcr_value"] = fcrValue
|
||||
recording.FcrValue = &fcrValue
|
||||
} else {
|
||||
@@ -1330,12 +1351,16 @@ func (s *recordingService) attachProductionStandard(ctx context.Context, item *e
|
||||
return nil
|
||||
}
|
||||
|
||||
week := ((int(*item.Day) - 1) / 7) + 1
|
||||
category := strings.ToUpper(item.ProjectFlockKandang.ProjectFlock.Category)
|
||||
weekBase := 1
|
||||
if category == string(utils.ProjectFlockCategoryLaying) {
|
||||
weekBase = 18
|
||||
}
|
||||
week := ((int(*item.Day) - 1) / 7) + weekBase
|
||||
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,6 +21,7 @@ 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"`
|
||||
|
||||
+2
-2
@@ -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, "Invalid request body")
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Format permintaan tidak valid")
|
||||
}
|
||||
|
||||
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: "Create transferLaying successfully",
|
||||
Message: "Berhasil membuat transfer laying",
|
||||
Data: dto.ToTransferLayingListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -67,8 +67,6 @@ 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"`
|
||||
@@ -166,7 +164,7 @@ func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouse
|
||||
func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO {
|
||||
return LayingTransferSourceDTO{
|
||||
SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang),
|
||||
Qty: source.Qty,
|
||||
Qty: source.UsageQty, // Ambil dari UsageQty (FIFO consumed quantity)
|
||||
ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse),
|
||||
Note: source.Note,
|
||||
}
|
||||
@@ -186,7 +184,7 @@ func ToLayingTransferSourceDTOs(sources []entity.LayingTransferSource) []LayingT
|
||||
func ToLayingTransferTargetDTO(target entity.LayingTransferTarget) LayingTransferTargetDTO {
|
||||
return LayingTransferTargetDTO{
|
||||
TargetProjectFlockKandang: ToProjectFlockKandangSummaryDTO(target.TargetProjectFlockKandang),
|
||||
Qty: target.Qty,
|
||||
Qty: target.TotalQty, // Ambil dari TotalQty (FIFO replenished quantity)
|
||||
ProductWarehouse: ToProductWarehouseSummaryDTO(target.ProductWarehouse),
|
||||
Note: target.Note,
|
||||
}
|
||||
@@ -223,8 +221,6 @@ 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,6 +26,8 @@ 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)
|
||||
@@ -36,30 +38,13 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
|
||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
||||
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
|
||||
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// daftarin jadi stockable
|
||||
if err := fifoService.RegisterStockable(fifo.StockableConfig{
|
||||
Key: fifo.StockableKeyTransferToLaying,
|
||||
Table: "laying_transfers",
|
||||
Key: fifo.StockableKeyTransferToLayingIn,
|
||||
Table: "laying_transfer_targets",
|
||||
Columns: fifo.StockableColumns{
|
||||
ID: "id",
|
||||
ProductWarehouseID: "dest_product_warehouse_id",
|
||||
ProductWarehouseID: "product_warehouse_id",
|
||||
TotalQuantity: "total_qty",
|
||||
TotalUsedQuantity: "total_used",
|
||||
CreatedAt: "created_at",
|
||||
@@ -71,6 +56,24 @@ 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 {
|
||||
@@ -79,6 +82,8 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
|
||||
|
||||
transferLayingService := sTransferLaying.NewTransferLayingService(
|
||||
transferLayingRepo,
|
||||
layingTransferSourceRepo,
|
||||
layingTransferTargetRepo,
|
||||
projectFlockRepo,
|
||||
projectFlockKandangRepo,
|
||||
projectFlockPopulationRepo,
|
||||
|
||||
+223
-297
@@ -16,6 +16,7 @@ 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"
|
||||
|
||||
@@ -40,17 +41,22 @@ 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,
|
||||
@@ -64,11 +70,14 @@ 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,
|
||||
}
|
||||
@@ -164,55 +173,42 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
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")
|
||||
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.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")
|
||||
sourceKandangIDs := make([]uint, len(req.SourceKandangs))
|
||||
for i, detail := range req.SourceKandangs {
|
||||
sourceKandangIDs[i] = detail.ProjectFlockKandangId
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
if err := s.validateKandangOwnership(
|
||||
c.Context(),
|
||||
req.SourceProjectFlockId,
|
||||
sourceKandangIDs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
targetKandangIDs := make([]uint, len(req.TargetKandangs))
|
||||
for i, detail := range req.TargetKandangs {
|
||||
targetKandangIDs[i] = detail.ProjectFlockKandangId
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
if err := s.validateKandangOwnership(
|
||||
c.Context(),
|
||||
req.TargetProjectFlockId,
|
||||
targetKandangIDs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transferDate, err := utils.ParseDateString(req.TransferDate)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transfer date format")
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Format tanggal transfer tidak valid")
|
||||
}
|
||||
|
||||
var totalSourceQty, totalTargetQty float64
|
||||
@@ -220,7 +216,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, "Source kandang quantity must be greater than 0")
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang sumber harus lebih dari 0")
|
||||
}
|
||||
totalSourceQty += sourceDetail.Quantity
|
||||
|
||||
@@ -239,11 +235,11 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
}
|
||||
|
||||
if totalPopulation == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no population available for transfer", sourceDetail.ProjectFlockKandangId))
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang sumber %d tidak memiliki populasi untuk ditransfer", sourceDetail.ProjectFlockKandangId))
|
||||
}
|
||||
|
||||
if 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))
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang sumber %d jumlah tidak mencukupi. Tersedia: %.0f, Diminta: %.0f", sourceDetail.ProjectFlockKandangId, totalPopulation, sourceDetail.Quantity))
|
||||
}
|
||||
|
||||
sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] = productWarehouseId
|
||||
@@ -251,13 +247,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, "Target kandang quantity must be greater than 0")
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang tujuan harus lebih dari 0")
|
||||
}
|
||||
totalTargetQty += targetDetail.Quantity
|
||||
}
|
||||
|
||||
if totalSourceQty != totalTargetQty {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total source quantity (%f) must equal total target quantity (%f)", totalSourceQty, totalTargetQty))
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Jumlah total sumber (%.0f) harus sama dengan jumlah total tujuan (%.0f)", totalSourceQty, totalTargetQty))
|
||||
}
|
||||
|
||||
transferNumber := fmt.Sprintf("TL-%d", time.Now().UnixNano())
|
||||
@@ -268,22 +264,18 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
FromProjectFlockId: req.SourceProjectFlockId,
|
||||
ToProjectFlockId: req.TargetProjectFlockId,
|
||||
TransferDate: transferDate,
|
||||
PendingUsageQty: &totalSourceQty,
|
||||
CreatedBy: actorID,
|
||||
}
|
||||
|
||||
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, "Failed to create transfer laying record")
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat record transfer laying")
|
||||
}
|
||||
|
||||
for _, sourceDetail := range req.SourceKandangs {
|
||||
@@ -292,78 +284,88 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
source := entity.LayingTransferSource{
|
||||
LayingTransferId: createBody.Id,
|
||||
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
|
||||
Qty: sourceDetail.Quantity,
|
||||
UsageQty: 0,
|
||||
PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval
|
||||
ProductWarehouseId: &productWarehouseId,
|
||||
}
|
||||
if err := dbTransaction.Create(&source).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer source")
|
||||
if err := sourceRepoTx.CreateOne(c.Context(), &source, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat sumber transfer")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var firstTargetProductWarehouseID *uint
|
||||
for _, targetDetail := range req.TargetKandangs {
|
||||
|
||||
for i, targetDetail := range req.TargetKandangs {
|
||||
|
||||
targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
|
||||
targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang")
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mendapatkan project flock kandang tujuan")
|
||||
}
|
||||
|
||||
targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId)
|
||||
targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetprojectFlockKandang.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))
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse tidak ditemukan untuk kandang tujuan %d", targetDetail.ProjectFlockKandangId))
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse")
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mendapatkan warehouse tujuan")
|
||||
}
|
||||
|
||||
var targetPW entity.ProductWarehouse
|
||||
err = dbTransaction.Where("warehouse_id = ? AND project_flock_kandang_id = ?", targetWarehouse.Id, targetDetail.ProjectFlockKandangId).
|
||||
First(&targetPW).Error
|
||||
// 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)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No product warehouse found for target kandang %d in warehouse %d", targetDetail.ProjectFlockKandangId, targetWarehouse.Id))
|
||||
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.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,
|
||||
Qty: targetDetail.Quantity,
|
||||
TotalQty: targetDetail.Quantity,
|
||||
TotalUsed: 0,
|
||||
ProductWarehouseId: &targetPW.Id,
|
||||
}
|
||||
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 := targetRepoTx.CreateOne(c.Context(), &target, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat target transfer")
|
||||
}
|
||||
}
|
||||
|
||||
if err := createApprovalTransferLaying(c.Context(), dbTransaction, createBody.Id, createBody.CreatedBy); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer approval")
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat approval transfer")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying")
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat transfer laying")
|
||||
}
|
||||
|
||||
return s.GetOne(c, createBody.Id)
|
||||
@@ -412,53 +414,32 @@ 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)
|
||||
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
|
||||
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||
sourceRepo := s.LayingTransferSourceRepo.WithTx(dbTransaction)
|
||||
targetRepo := s.LayingTransferTargetRepo.WithTx(dbTransaction)
|
||||
|
||||
// Hapus old sources dan targets
|
||||
for _, oldSource := range existingTransfer.Sources {
|
||||
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 {
|
||||
if err := sourceRepo.DeleteOne(c.Context(), oldSource.Id); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete old source")
|
||||
}
|
||||
}
|
||||
|
||||
for _, oldTarget := range existingTransfer.Targets {
|
||||
if err := dbTransaction.Delete(&oldTarget).Error; err != nil {
|
||||
if err := targetRepo.DeleteOne(c.Context(), oldTarget.Id); 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,
|
||||
"pending_usage_qty": &totalSourceQty,
|
||||
"transfer_date": transferDate,
|
||||
"notes": req.Reason,
|
||||
}, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer header")
|
||||
}
|
||||
|
||||
sourceWarehouseMap := make(map[uint]uint)
|
||||
// Create new sources dengan pending quantity
|
||||
for _, sourceDetail := range req.SourceKandangs {
|
||||
|
||||
populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId)
|
||||
populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations")
|
||||
}
|
||||
@@ -467,48 +448,39 @@ 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 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))
|
||||
if productWarehouseId == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no product warehouse", sourceDetail.ProjectFlockKandangId))
|
||||
}
|
||||
|
||||
sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] = productWarehouseId
|
||||
|
||||
source := entity.LayingTransferSource{
|
||||
LayingTransferId: id,
|
||||
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
|
||||
Qty: sourceDetail.Quantity,
|
||||
UsageQty: 0,
|
||||
PendingUsageQty: sourceDetail.Quantity,
|
||||
ProductWarehouseId: &productWarehouseId,
|
||||
}
|
||||
if err := dbTransaction.Create(&source).Error; err != nil {
|
||||
if err := sourceRepo.CreateOne(c.Context(), &source, nil); 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 {
|
||||
targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
|
||||
targetprojectFlockKandang, 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(), targetPFK.KandangId)
|
||||
targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetprojectFlockKandang.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))
|
||||
@@ -516,23 +488,50 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse")
|
||||
}
|
||||
|
||||
var targetPW entity.ProductWarehouse
|
||||
err = dbTransaction.Where("warehouse_id = ? AND project_flock_kandang_id = ?", targetWarehouse.Id, targetDetail.ProjectFlockKandangId).
|
||||
First(&targetPW).Error
|
||||
// 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)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No product warehouse found for target kandang %d in warehouse %d", targetDetail.ProjectFlockKandangId, targetWarehouse.Id))
|
||||
|
||||
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.StatusInternalServerError, fmt.Sprintf("Failed to get product warehouse for target kandang %d: %v", targetDetail.ProjectFlockKandangId, err))
|
||||
}
|
||||
|
||||
target := entity.LayingTransferTarget{
|
||||
LayingTransferId: id,
|
||||
TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId,
|
||||
Qty: targetDetail.Quantity,
|
||||
TotalQty: targetDetail.Quantity,
|
||||
TotalUsed: 0,
|
||||
ProductWarehouseId: &targetPW.Id,
|
||||
}
|
||||
if err := dbTransaction.Create(&target).Error; err != nil {
|
||||
if err := targetRepo.CreateOne(c.Context(), &target, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer target")
|
||||
}
|
||||
}
|
||||
@@ -560,6 +559,7 @@ 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,48 +573,6 @@ 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")
|
||||
@@ -667,6 +625,8 @@ 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)
|
||||
|
||||
@@ -691,70 +651,73 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
|
||||
}
|
||||
|
||||
if action == entity.ApprovalActionApproved && transfer.PendingUsageQty != nil && *transfer.PendingUsageQty > 0 {
|
||||
if action == entity.ApprovalActionApproved {
|
||||
|
||||
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), approvableID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer sources")
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil sources transfer")
|
||||
}
|
||||
|
||||
targets, err := targetRepoTx.GetByLayingTransferId(c.Context(), approvableID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer targets")
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil targets transfer")
|
||||
}
|
||||
|
||||
if len(sources) > 0 && len(targets) > 0 {
|
||||
// Hitung total quantity dari targets untuk di-consume dari sources
|
||||
totalTargetQty := 0.0
|
||||
for _, target := range targets {
|
||||
totalTargetQty += target.TotalQty
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
// 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))
|
||||
}
|
||||
|
||||
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: ¬e,
|
||||
Tx: dbTransaction,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock to destination warehouse: %v", 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 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")
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
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: ¬e,
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -820,9 +783,8 @@ func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context,
|
||||
newWarehouse := &entity.ProductWarehouse{
|
||||
ProductId: productID,
|
||||
WarehouseId: warehouseID,
|
||||
ProjectFlockKandangId: projectFlockKandangId, // Set flock ID agar bisa di-chickin di target flock
|
||||
ProjectFlockKandangId: projectFlockKandangId,
|
||||
Quantity: quantity,
|
||||
// CreatedBy: actorID,
|
||||
}
|
||||
|
||||
if err := productWarehouseRepoTx.CreateOne(ctx, newWarehouse, nil); err != nil {
|
||||
@@ -832,66 +794,6 @@ 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 {
|
||||
@@ -925,3 +827,27 @@ 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 LIKE ?", column), prefix+"%").
|
||||
Where(fmt.Sprintf("%s ILIKE ?", column), prefix+"%").
|
||||
Select(column).
|
||||
Order(fmt.Sprintf("%s DESC", column)).
|
||||
Limit(20).
|
||||
|
||||
@@ -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("/", 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)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -618,7 +618,10 @@ func (b *expenseBridge) createExpenseViaService(
|
||||
actorID = 1
|
||||
}
|
||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
||||
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepManager, &action, actorID, nil); err != nil {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ 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"
|
||||
@@ -81,6 +82,7 @@ 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", ""),
|
||||
@@ -164,6 +166,59 @@ 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 {
|
||||
@@ -227,3 +282,27 @@ 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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
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,12 +1,16 @@
|
||||
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"
|
||||
@@ -22,7 +26,7 @@ type RepportMarketingItemDTO struct {
|
||||
DoNumber string `json:"do_number"`
|
||||
Sales *userDTO.UserRelationDTO `json:"sales,omitempty"`
|
||||
VehicleNumber string `json:"vehicle_number"`
|
||||
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
|
||||
Product *ProductRelationDTOFixed `json:"product,omitempty"`
|
||||
MarketingType string `json:"marketing_type"`
|
||||
Qty float64 `json:"qty"`
|
||||
AverageWeightKg float64 `json:"average_weight_kg"`
|
||||
@@ -46,6 +50,12 @@ 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
|
||||
@@ -106,7 +116,7 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK
|
||||
|
||||
if mdp.MarketingProduct.ProductWarehouse.ProductId != 0 {
|
||||
mapped := productDTO.ToProductRelationDTO(mdp.MarketingProduct.ProductWarehouse.Product)
|
||||
item.Product = &mapped
|
||||
item.Product = newProductRelationDTOFixedPtr(&mapped)
|
||||
}
|
||||
|
||||
return item
|
||||
@@ -139,7 +149,7 @@ func ToRepportMarketingItemDTOsWithHppMap(mdps []entity.MarketingDeliveryProduct
|
||||
}
|
||||
|
||||
func getMarketingType(mdp entity.MarketingDeliveryProduct) string {
|
||||
hasAyam, hasTelur := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
|
||||
hasAyam, hasTelur, hasTrading := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
|
||||
|
||||
if hasAyam {
|
||||
return "ayam"
|
||||
@@ -147,12 +157,15 @@ func getMarketingType(mdp entity.MarketingDeliveryProduct) string {
|
||||
if hasTelur {
|
||||
return "telur"
|
||||
}
|
||||
return "trading"
|
||||
if hasTrading {
|
||||
return "trading"
|
||||
}
|
||||
return "trading" // default to trading if no flags found
|
||||
}
|
||||
|
||||
func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur bool) {
|
||||
func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur, hasTrading bool) {
|
||||
if len(flags) == 0 {
|
||||
return false, false
|
||||
return false, false, false
|
||||
}
|
||||
|
||||
for _, flag := range flags {
|
||||
@@ -167,13 +180,18 @@ func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur 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
|
||||
return hasAyam, hasTelur, hasTrading
|
||||
}
|
||||
|
||||
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
|
||||
@@ -259,3 +277,49 @@ 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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -31,12 +31,13 @@ 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, hppPerKandangRepository, productionResultRepository)
|
||||
repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, debtSupplierRepository, 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
Reference in New Issue
Block a user