mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Compare commits
179 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 32a8557a3b | |||
| e32b231c6c | |||
| ca6d0b160b | |||
| adabb4e035 | |||
| 67ecdbc1dd | |||
| e6244dea8a | |||
| d1e4bf060e | |||
| fc06b3e4db | |||
| 8ff97cb647 | |||
| 1b7ce3c62c | |||
| 21de17b18a | |||
| 2aaaab91f7 | |||
| 4227152979 | |||
| ec020ac17c | |||
| adc30ad5cd | |||
| 9fb5395469 | |||
| bc771660be | |||
| b615570036 | |||
| c793c3cf9a | |||
| b3e0410f5a | |||
| a882d5a687 | |||
| c0848b6d2d | |||
| b240478ed5 | |||
| 3052497fc0 | |||
| 71c62c5e02 | |||
| 768961d7d6 | |||
| 8cd9627a51 | |||
| 378d633ea4 | |||
| fb193fc61f | |||
| af7aabdec8 | |||
| bac36b4f00 | |||
| 687d02313b | |||
| 7d3602d829 | |||
| 533e9aca6f | |||
| dcfb5e10b4 | |||
| fbeccf4cdc | |||
| f2ae2cc731 | |||
| eda50930e7 | |||
| 0bc5480a1d | |||
| ef482dd1b9 | |||
| 302f0ed877 | |||
| 31c48ee1da | |||
| 8ad11af9c9 | |||
| 688d3fa757 | |||
| 409652c15e | |||
| 08be60c229 | |||
| 50b19dc1c3 | |||
| e770526c1a | |||
| 77af262662 | |||
| 9d611b7492 | |||
| 21ff1c8ab7 | |||
| 2ca84ecffe | |||
| f13e4f907c | |||
| 4d334e8d5c | |||
| 62522a751f | |||
| 62ccc2e5d6 | |||
| 13fc246f21 | |||
| 89293a843e | |||
| a5af469865 | |||
| 8efe9b668b | |||
| 89480deeb0 | |||
| 4146342120 | |||
| b375fb964e | |||
| fe002c9602 | |||
| f1032b44d1 | |||
| 7f2401311b | |||
| 3f4d6c630a | |||
| 8792161c02 | |||
| 37c26d5877 | |||
| c316a6d7a9 | |||
| 3a89e18b16 | |||
| c6dc94a4e1 | |||
| aeb5433346 | |||
| cad15bcd78 | |||
| e004354420 | |||
| e0ff6e6d79 | |||
| 804ff45dbd | |||
| 9f1c153841 | |||
| 2a884a8d09 | |||
| dbf72c7248 | |||
| 7daa509cd0 | |||
| 894fa0b22a | |||
| d680196919 | |||
| 9fc2d0556e | |||
| c2a89910fb | |||
| 1847e5590a | |||
| 57094f664c | |||
| f8b6e12d16 | |||
| e12c34db13 | |||
| 3012d260ec | |||
| f6e872c0aa | |||
| 8a639f127c | |||
| 2acaa10b60 | |||
| bd9d41e161 | |||
| 06d8d0b795 | |||
| 1d5e7b6e1a | |||
| da190f1b05 | |||
| c8905eb715 | |||
| 7f1d796b65 | |||
| 6c7ff3f415 | |||
| 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 | |||
| bba2dec8c6 | |||
| f7b70d4b14 | |||
| 9f28294dc3 | |||
| 6a166ceb86 | |||
| f0b4fe916c | |||
| 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
|
||||
|
||||
+17
-87
@@ -1,90 +1,20 @@
|
||||
stages:
|
||||
- deploy
|
||||
workflow:
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
- if: '$CI_COMMIT_BRANCH == "development"'
|
||||
- if: '$CI_COMMIT_BRANCH == "staging"'
|
||||
- if: '$CI_COMMIT_BRANCH == "production"'
|
||||
- when: never
|
||||
|
||||
deploy-dev:
|
||||
stage: deploy
|
||||
image: alpine:3.20
|
||||
variables:
|
||||
DEPLOY_APP: "LTI-MBUGROUP"
|
||||
# Opsional: kalau pakai submodule, ini bikin clone submodule pakai SSH juga
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
GIT_DEPTH: "1"
|
||||
include:
|
||||
- local: "ci/development.yml"
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "development"'
|
||||
|
||||
before_script:
|
||||
- echo "🧰 Installing dependencies..."
|
||||
- apk update && apk add --no-cache openssh git curl bash
|
||||
- local: "ci/staging.yml"
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "staging"'
|
||||
|
||||
# Setup SSH di runner
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
|
||||
- chmod 600 ~/.ssh/id_rsa
|
||||
- eval "$(ssh-agent -s)"
|
||||
- ssh-add ~/.ssh/id_rsa
|
||||
|
||||
# Trust host keys (server + gitlab) biar SSH gak nanya interaktif
|
||||
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
|
||||
- ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
|
||||
|
||||
script:
|
||||
- echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP"
|
||||
|
||||
- >
|
||||
if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" "
|
||||
set -e
|
||||
|
||||
cd /home/devops/docker/deployment/development/lti-api
|
||||
|
||||
# Pastikan remote origin SSH (antisipasi kalau pernah ke-set HTTPS)
|
||||
git remote set-url origin git@gitlab.com:mbugroup/lti-api.git
|
||||
|
||||
# Pastikan server percaya gitlab.com juga (untuk git fetch via SSH)
|
||||
mkdir -p ~/.ssh
|
||||
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
|
||||
|
||||
# Fetch/reset pakai SSH
|
||||
GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' git fetch origin development
|
||||
git reset --hard origin/development
|
||||
|
||||
docker compose restart dev-api-lti || docker compose up -d dev-api-lti
|
||||
"; then
|
||||
STATUS='success';
|
||||
else
|
||||
STATUS='failed';
|
||||
fi;
|
||||
|
||||
RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}";
|
||||
|
||||
if [ "$STATUS" = "success" ]; then
|
||||
COLOR=3066993;
|
||||
TITLE="✅ Deployment API Succeeded";
|
||||
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully.";
|
||||
else
|
||||
COLOR=15158332;
|
||||
TITLE="❌ Deployment API Failed Gaes";
|
||||
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` failed.";
|
||||
fi;
|
||||
|
||||
echo "{
|
||||
\"username\": \"CI Bot\",
|
||||
\"embeds\": [{
|
||||
\"title\": \"$TITLE\",
|
||||
\"description\": \"$DESC\",
|
||||
\"color\": $COLOR,
|
||||
\"fields\": [
|
||||
{\"name\": \"Repository\", \"value\": \"${CI_PROJECT_PATH}\", \"inline\": true},
|
||||
{\"name\": \"Actor\", \"value\": \"${GITLAB_USER_LOGIN}\", \"inline\": true},
|
||||
{\"name\": \"Commit\", \"value\": \"${CI_COMMIT_SHA}\", \"inline\": false},
|
||||
{\"name\": \"Pipeline\", \"value\": \"[Open run](${RUN_URL})\", \"inline\": false}
|
||||
]
|
||||
}]
|
||||
}" > payload.json;
|
||||
|
||||
echo "📡 Sending notification to Discord...";
|
||||
curl -sS -H "Content-Type: application/json" \
|
||||
-d @payload.json "$DISCORD_WEBHOOK_URL";
|
||||
|
||||
only:
|
||||
- development
|
||||
|
||||
environment:
|
||||
name: development
|
||||
- local: "ci/production.yml"
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "production"'
|
||||
|
||||
+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"]
|
||||
|
||||
@@ -110,4 +110,4 @@ IT Development PT Mitra Berlian Unggas Group
|
||||
|
||||
## 📃 License
|
||||
|
||||
This project is private. All rights reserved.
|
||||
> This project is private. All rights reserved.
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
stages:
|
||||
- deploy
|
||||
|
||||
deploy-dev:
|
||||
stage: deploy
|
||||
image: alpine:3.20
|
||||
variables:
|
||||
DEPLOY_APP: "LTI-MBUGROUP"
|
||||
# Opsional: kalau pakai submodule, ini bikin clone submodule pakai SSH juga
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
GIT_DEPTH: "1"
|
||||
|
||||
before_script:
|
||||
- echo "🧰 Installing dependencies..."
|
||||
- apk update && apk add --no-cache openssh git curl bash
|
||||
|
||||
# Setup SSH di runner
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
|
||||
- chmod 600 ~/.ssh/id_rsa
|
||||
- eval "$(ssh-agent -s)"
|
||||
- ssh-add ~/.ssh/id_rsa
|
||||
|
||||
# Trust host keys (server + gitlab) biar SSH gak nanya interaktif
|
||||
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
|
||||
- ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
|
||||
|
||||
script:
|
||||
- echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP"
|
||||
|
||||
- >
|
||||
if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" "
|
||||
set -e
|
||||
|
||||
cd /home/devops/docker/deployment/development/lti-api
|
||||
|
||||
# Pastikan remote origin SSH (antisipasi kalau pernah ke-set HTTPS)
|
||||
git remote set-url origin git@gitlab.com:mbugroup/lti-api.git
|
||||
|
||||
# Pastikan server percaya gitlab.com juga (untuk git fetch via SSH)
|
||||
mkdir -p ~/.ssh
|
||||
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
|
||||
|
||||
# Fetch/reset pakai SSH
|
||||
GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' git fetch origin development
|
||||
git reset --hard origin/development
|
||||
|
||||
docker compose restart dev-api-lti || docker compose up -d dev-api-lti
|
||||
"; then
|
||||
STATUS='success';
|
||||
else
|
||||
STATUS='failed';
|
||||
fi;
|
||||
|
||||
RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}";
|
||||
|
||||
if [ "$STATUS" = "success" ]; then
|
||||
COLOR=3066993;
|
||||
TITLE="✅ Deployment API Succeeded";
|
||||
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully.";
|
||||
else
|
||||
COLOR=15158332;
|
||||
TITLE="❌ Deployment API Failed Gaes";
|
||||
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` failed.";
|
||||
fi;
|
||||
|
||||
echo "{
|
||||
\"username\": \"CI Bot\",
|
||||
\"embeds\": [{
|
||||
\"title\": \"$TITLE\",
|
||||
\"description\": \"$DESC\",
|
||||
\"color\": $COLOR,
|
||||
\"fields\": [
|
||||
{\"name\": \"Repository\", \"value\": \"${CI_PROJECT_PATH}\", \"inline\": true},
|
||||
{\"name\": \"Actor\", \"value\": \"${GITLAB_USER_LOGIN}\", \"inline\": true},
|
||||
{\"name\": \"Commit\", \"value\": \"${CI_COMMIT_SHA}\", \"inline\": false},
|
||||
{\"name\": \"Pipeline\", \"value\": \"[Open run](${RUN_URL})\", \"inline\": false}
|
||||
]
|
||||
}]
|
||||
}" > payload.json;
|
||||
|
||||
echo "📡 Sending notification to Discord...";
|
||||
curl -sS -H "Content-Type: application/json" \
|
||||
-d @payload.json "$DISCORD_WEBHOOK_URL";
|
||||
|
||||
only:
|
||||
- development
|
||||
|
||||
environment:
|
||||
name: development
|
||||
@@ -0,0 +1,133 @@
|
||||
stages:
|
||||
- build
|
||||
- migrate
|
||||
- deploy
|
||||
- seed
|
||||
|
||||
default:
|
||||
tags:
|
||||
- self-hosted-prod
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
|
||||
when: always
|
||||
- when: never
|
||||
|
||||
variables:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
|
||||
IMAGE_TAG: "production_${CI_COMMIT_SHORT_SHA}"
|
||||
IMAGE_NAME: "${CI_REGISTRY_IMAGE}:${IMAGE_TAG}"
|
||||
IMAGE_LATEST: "${CI_REGISTRY_IMAGE}:production_latest"
|
||||
|
||||
DEPLOY_DIR: "/opt/deploy/lti"
|
||||
COMPOSE_FILE: "docker-compose.yaml"
|
||||
|
||||
# =========================
|
||||
# BUILD (AUTO)
|
||||
# =========================
|
||||
build_production:
|
||||
stage: build
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
|
||||
script: |
|
||||
set -e
|
||||
docker info
|
||||
|
||||
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
||||
|
||||
echo "✅ Build image: $IMAGE_NAME"
|
||||
docker build -t "$IMAGE_NAME" -f Dockerfile .
|
||||
|
||||
echo "✅ Push image: $IMAGE_NAME"
|
||||
docker push "$IMAGE_NAME"
|
||||
|
||||
echo "✅ Tag latest: $IMAGE_LATEST"
|
||||
docker tag "$IMAGE_NAME" "$IMAGE_LATEST"
|
||||
docker push "$IMAGE_LATEST"
|
||||
|
||||
|
||||
# =========================
|
||||
# MIGRATE (PRODUCTION - MANUAL)
|
||||
# =========================
|
||||
migrate_production:
|
||||
stage: migrate
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
|
||||
when: manual
|
||||
allow_failure: false
|
||||
needs:
|
||||
- job: build_production
|
||||
artifacts: false
|
||||
script: |
|
||||
set -e
|
||||
cd /opt/deploy/lti
|
||||
test -f .env || (echo "❌ .env not found" && exit 1)
|
||||
|
||||
set -a
|
||||
. ./.env
|
||||
set +a
|
||||
|
||||
# Validasi env wajib
|
||||
: "${DB_HOST:?DB_HOST not set}"
|
||||
: "${DB_PORT:?DB_PORT not set}"
|
||||
: "${DB_USER:?DB_USER not set}"
|
||||
: "${DB_PASSWORD:?DB_PASSWORD not set}"
|
||||
: "${DB_NAME:?DB_NAME not set}"
|
||||
|
||||
DB_SSLMODE="${DB_SSLMODE:-require}"
|
||||
export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE}"
|
||||
|
||||
echo "✅ Running migrations (production)..."
|
||||
docker run --rm \
|
||||
-v "$CI_PROJECT_DIR/internal/database/migrations:/migrations:ro" \
|
||||
migrate/migrate:v4.15.2 \
|
||||
-path=/migrations -database "$DATABASE_URL" up
|
||||
|
||||
|
||||
# =========================
|
||||
# DEPLOY (AUTO)
|
||||
# =========================
|
||||
deploy_production:
|
||||
stage: deploy
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
|
||||
needs:
|
||||
- job: migrate_production
|
||||
artifacts: false
|
||||
- job: build_production
|
||||
artifacts: false
|
||||
script: |
|
||||
set -e
|
||||
docker info
|
||||
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
||||
|
||||
cd "$DEPLOY_DIR"
|
||||
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1)
|
||||
test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1)
|
||||
|
||||
docker compose -f "$COMPOSE_FILE" pull
|
||||
docker compose -f "$COMPOSE_FILE" up -d --force-recreate
|
||||
docker image prune -f
|
||||
|
||||
|
||||
# =========================
|
||||
# SEED (MANUAL)
|
||||
# =========================
|
||||
seed_production:
|
||||
stage: seed
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "production"'
|
||||
when: manual
|
||||
script: |
|
||||
set -e
|
||||
cd /opt/deploy/lti
|
||||
test -f .env || (echo "❌ .env not found" && exit 1)
|
||||
|
||||
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
||||
|
||||
docker compose --env-file .env pull seed
|
||||
docker compose --env-file .env run --rm seed
|
||||
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
stages:
|
||||
- build
|
||||
- migrate
|
||||
- deploy
|
||||
- seed
|
||||
|
||||
default:
|
||||
tags:
|
||||
- self-hosted-prod
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
|
||||
when: always
|
||||
- when: never
|
||||
|
||||
variables:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
|
||||
IMAGE_TAG: "production_${CI_COMMIT_SHORT_SHA}"
|
||||
IMAGE_NAME: "${CI_REGISTRY_IMAGE}:${IMAGE_TAG}"
|
||||
IMAGE_LATEST: "${CI_REGISTRY_IMAGE}:production_latest"
|
||||
|
||||
DEPLOY_DIR: "/opt/deploy/lti"
|
||||
COMPOSE_FILE: "docker-compose.yaml"
|
||||
|
||||
# =========================
|
||||
# BUILD (AUTO)
|
||||
# =========================
|
||||
build_production:
|
||||
stage: build
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
|
||||
script: |
|
||||
set -e
|
||||
docker info
|
||||
|
||||
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
||||
|
||||
echo "✅ Build image: $IMAGE_NAME"
|
||||
docker build -t "$IMAGE_NAME" -f Dockerfile .
|
||||
|
||||
echo "✅ Push image: $IMAGE_NAME"
|
||||
docker push "$IMAGE_NAME"
|
||||
|
||||
echo "✅ Tag latest: $IMAGE_LATEST"
|
||||
docker tag "$IMAGE_NAME" "$IMAGE_LATEST"
|
||||
docker push "$IMAGE_LATEST"
|
||||
|
||||
|
||||
# =========================
|
||||
# MIGRATE (PRODUCTION - MANUAL)
|
||||
# =========================
|
||||
migrate_production:
|
||||
stage: migrate
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
|
||||
when: manual
|
||||
allow_failure: false
|
||||
needs:
|
||||
- job: build_production
|
||||
artifacts: false
|
||||
script: |
|
||||
set -e
|
||||
cd /opt/deploy/lti
|
||||
test -f .env || (echo "❌ .env not found" && exit 1)
|
||||
|
||||
set -a
|
||||
. ./.env
|
||||
set +a
|
||||
|
||||
# Validasi env wajib
|
||||
: "${DB_HOST:?DB_HOST not set}"
|
||||
: "${DB_PORT:?DB_PORT not set}"
|
||||
: "${DB_USER:?DB_USER not set}"
|
||||
: "${DB_PASSWORD:?DB_PASSWORD not set}"
|
||||
: "${DB_NAME:?DB_NAME not set}"
|
||||
|
||||
DB_SSLMODE="${DB_SSLMODE:-require}"
|
||||
export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE}"
|
||||
|
||||
echo "✅ Running migrations (production)..."
|
||||
docker run --rm \
|
||||
-v "$CI_PROJECT_DIR/internal/database/migrations:/migrations:ro" \
|
||||
migrate/migrate:v4.15.2 \
|
||||
-path=/migrations -database "$DATABASE_URL" up
|
||||
|
||||
|
||||
# =========================
|
||||
# DEPLOY (AUTO)
|
||||
# =========================
|
||||
deploy_production:
|
||||
stage: deploy
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
|
||||
needs:
|
||||
- job: migrate_production
|
||||
artifacts: false
|
||||
- job: build_production
|
||||
artifacts: false
|
||||
script: |
|
||||
set -e
|
||||
docker info
|
||||
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
||||
|
||||
cd "$DEPLOY_DIR"
|
||||
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1)
|
||||
test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1)
|
||||
|
||||
docker compose -f "$COMPOSE_FILE" pull
|
||||
docker compose -f "$COMPOSE_FILE" up -d --force-recreate
|
||||
docker image prune -f
|
||||
|
||||
|
||||
# =========================
|
||||
# SEED (MANUAL)
|
||||
# =========================
|
||||
seed_production:
|
||||
stage: seed
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "production"'
|
||||
when: manual
|
||||
script: |
|
||||
set -e
|
||||
cd /opt/deploy/lti
|
||||
test -f .env || (echo "❌ .env not found" && exit 1)
|
||||
|
||||
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
||||
|
||||
docker compose --env-file .env pull seed
|
||||
docker compose --env-file .env run --rm seed
|
||||
|
||||
|
||||
@@ -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,3 @@
|
||||
ALTER TABLE recording_eggs
|
||||
DROP COLUMN IF EXISTS total_used,
|
||||
DROP COLUMN IF EXISTS total_qty;
|
||||
@@ -0,0 +1,7 @@
|
||||
ALTER TABLE recording_eggs
|
||||
ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0 NOT NULL;
|
||||
|
||||
UPDATE recording_eggs
|
||||
SET total_qty = qty
|
||||
WHERE total_qty = 0;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- Rollback: add price back to nonstock_suppliers
|
||||
ALTER TABLE nonstock_suppliers
|
||||
ADD COLUMN IF NOT EXISTS price NUMERIC(15, 3) NOT NULL DEFAULT 0;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- Migration: remove price from nonstock_suppliers
|
||||
ALTER TABLE nonstock_suppliers
|
||||
DROP COLUMN IF EXISTS price;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
-- Rollback: Remove requested_qty column from laying_transfer_sources table
|
||||
|
||||
ALTER TABLE laying_transfer_sources
|
||||
DROP COLUMN IF EXISTS requested_qty;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
-- Add requested_qty column to laying_transfer_sources table
|
||||
-- This field stores the quantity requested by user during create/update
|
||||
-- Separate from UsageQty (FIFO consumed) and PendingUsageQty (FIFO pending)
|
||||
|
||||
ALTER TABLE laying_transfer_sources
|
||||
ADD COLUMN requested_qty NUMERIC(15,3) DEFAULT 0 NOT NULL;
|
||||
|
||||
-- Add comment for documentation
|
||||
COMMENT ON COLUMN laying_transfer_sources.requested_qty IS 'Quantity requested by user during create/update';
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE recording_depletions
|
||||
DROP COLUMN IF EXISTS pending_qty,
|
||||
DROP COLUMN IF EXISTS source_product_warehouse_id;
|
||||
@@ -0,0 +1,17 @@
|
||||
ALTER TABLE recording_depletions
|
||||
ADD COLUMN IF NOT EXISTS pending_qty numeric(15,3) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS source_product_warehouse_id bigint;
|
||||
|
||||
UPDATE recording_depletions rd
|
||||
SET source_product_warehouse_id = src.product_warehouse_id
|
||||
FROM recordings r
|
||||
JOIN LATERAL (
|
||||
SELECT pfp.product_warehouse_id
|
||||
FROM project_chickins pc
|
||||
JOIN project_flock_populations pfp ON pfp.project_chickin_id = pc.id
|
||||
WHERE pc.project_flock_kandang_id = r.project_flock_kandangs_id
|
||||
ORDER BY pfp.created_at ASC, pfp.id ASC
|
||||
LIMIT 1
|
||||
) AS src ON true
|
||||
WHERE r.id = rd.recording_id
|
||||
AND rd.source_product_warehouse_id IS NULL;
|
||||
@@ -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,9 @@ 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"`
|
||||
RequestedQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // Quantity requested by user
|
||||
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"`
|
||||
|
||||
@@ -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,10 +1,12 @@
|
||||
package entities
|
||||
|
||||
type RecordingDepletion struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||
Qty float64 `gorm:"column:qty;not null"`
|
||||
Id uint `gorm:"primaryKey"`
|
||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||
SourceProductWarehouseId *uint `gorm:"column:source_product_warehouse_id"`
|
||||
Qty float64 `gorm:"column:qty;not null"`
|
||||
PendingQty float64 `gorm:"column:pending_qty"`
|
||||
|
||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||
|
||||
@@ -7,6 +7,8 @@ type RecordingEgg struct {
|
||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||
Qty int `gorm:"column:qty;not null"`
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
TotalUsed float64 `gorm:"column:total_used"`
|
||||
Weight *float64 `gorm:"column:weight"`
|
||||
CreatedBy uint `gorm:"column:created_by"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package middleware
|
||||
|
||||
const (
|
||||
P_DashboardGetAll = "lti.dashboard.list"
|
||||
)
|
||||
|
||||
// project-flock
|
||||
const (
|
||||
P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing"
|
||||
@@ -19,18 +23,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 +49,10 @@ 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"
|
||||
P_ReportCustomerPaymentGetAll = "lti.repport.customerpayment.list"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -134,17 +142,17 @@ const (
|
||||
P_NonstocksUpdateOne = "lti.master.nonstocks.update"
|
||||
P_NonstocksDeleteOne = "lti.master.nonstocks.delete"
|
||||
|
||||
P_ProductCategoriesGetAll = "lti.master.Product_categories.list"
|
||||
P_ProductCategoriesGetOne = "lti.master.Product_categories.detail"
|
||||
P_ProductCategoriesCreateOne = "lti.master.Product_categories.create"
|
||||
P_ProductCategoriesUpdateOne = "lti.master.Product_categories.update"
|
||||
P_ProductCategoriesDeleteOne = "lti.master.Product_categories.delete"
|
||||
P_ProductCategoriesGetAll = "lti.master.product_categories.list"
|
||||
P_ProductCategoriesGetOne = "lti.master.product_categories.detail"
|
||||
P_ProductCategoriesCreateOne = "lti.master.product_categories.create"
|
||||
P_ProductCategoriesUpdateOne = "lti.master.product_categories.update"
|
||||
P_ProductCategoriesDeleteOne = "lti.master.product_categories.delete"
|
||||
|
||||
P_ProductsGetAll = "lti.master.Products.list"
|
||||
P_ProductsGetOne = "lti.master.Products.detail"
|
||||
P_ProductsCreateOne = "lti.master.Products.create"
|
||||
P_ProductsUpdateOne = "lti.master.Products.update"
|
||||
P_ProductsDeleteOne = "lti.master.Products.delete"
|
||||
P_ProductsGetAll = "lti.master.products.list"
|
||||
P_ProductsGetOne = "lti.master.products.detail"
|
||||
P_ProductsCreateOne = "lti.master.products.create"
|
||||
P_ProductsUpdateOne = "lti.master.products.update"
|
||||
P_ProductsDeleteOne = "lti.master.products.delete"
|
||||
|
||||
P_SuppliersGetAll = "lti.master.suppliers.list"
|
||||
P_SuppliersGetOne = "lti.master.suppliers.detail"
|
||||
@@ -207,15 +215,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 (
|
||||
@@ -232,3 +240,15 @@ const (
|
||||
P_UserGetAll = "lti.users.list"
|
||||
P_UserGetOne = "lti.users.detail"
|
||||
)
|
||||
|
||||
// daily-checklist
|
||||
const (
|
||||
P_DailyChecklistDashboardList = "lti.daily_checklist.dashboard.list"
|
||||
P_DailyChecklistCreateOne = "lti.daily_checklist.create"
|
||||
P_DailyChecklistGetAll = "lti.daily_checklist.list"
|
||||
P_DailyChecklistGetOne = "lti.daily_checklist.detail"
|
||||
P_DailyChecklistReports = "lti.daily_checklist.reports"
|
||||
P_DailyChecklistEmployee = "lti.daily_checklist.master_data.employee"
|
||||
P_DailyChecklistActivity = "lti.daily_checklist.master_data.activity"
|
||||
P_DailyChecklistActivityConfig = "lti.daily_checklist.master_data.configuration"
|
||||
)
|
||||
|
||||
@@ -14,14 +14,16 @@ import (
|
||||
)
|
||||
|
||||
type ClosingController struct {
|
||||
ClosingService service.ClosingService
|
||||
SapronakService service.SapronakService
|
||||
ClosingService service.ClosingService
|
||||
SapronakService service.SapronakService
|
||||
ClosingKeuanganService service.ClosingKeuanganService
|
||||
}
|
||||
|
||||
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService) *ClosingController {
|
||||
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService, closingKeuanganService service.ClosingKeuanganService) *ClosingController {
|
||||
return &ClosingController{
|
||||
ClosingService: closingService,
|
||||
SapronakService: sapronakService,
|
||||
ClosingService: closingService,
|
||||
SapronakService: sapronakService,
|
||||
ClosingKeuanganService: closingKeuanganService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +80,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 +118,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 +150,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 +160,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
|
||||
}
|
||||
@@ -158,9 +236,18 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
query := &validation.ClosingSapronakQuery{
|
||||
Type: strings.ToLower(c.Query("type")),
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Type: strings.ToLower(c.Query("type")),
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search"),
|
||||
}
|
||||
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 {
|
||||
@@ -191,6 +278,45 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetClosingSapronakSummary(c *fiber.Ctx) error {
|
||||
param := c.Params("projectFlockId")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil || id <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
||||
}
|
||||
|
||||
query := &validation.ClosingSapronakQuery{
|
||||
Type: strings.ToLower(c.Query("type")),
|
||||
Search: c.Query("search"),
|
||||
}
|
||||
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.Type != validation.SapronakTypeIncoming && query.Type != validation.SapronakTypeOutgoing {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
|
||||
}
|
||||
|
||||
result, err := u.ClosingService.GetClosingSapronakSummary(c, uint(id), query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Retrieved closing report (sapronak summary) successfully",
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error {
|
||||
param := c.Params("project_flock_id")
|
||||
flag := c.Query("flag", "")
|
||||
@@ -254,7 +380,7 @@ func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
||||
}
|
||||
|
||||
result, err := u.ClosingService.GetClosingKeuangan(c, uint(projectFlockID))
|
||||
result, err := u.ClosingKeuanganService.GetClosingKeuangan(c, uint(projectFlockID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -268,6 +394,34 @@ func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetClosingKeuanganByKandang(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")
|
||||
}
|
||||
|
||||
result, err := u.ClosingKeuanganService.GetClosingKeuanganByKandang(c, uint(projectFlockID), uint(pfkID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get closing keuangan by kandang successfully",
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error {
|
||||
param := c.Params("project_flock_id")
|
||||
|
||||
@@ -338,7 +492,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"`
|
||||
EggMass *float64 `json:"egg_mass,omitempty"`
|
||||
EggMassStd float64 `json:"egg_mass_std"`
|
||||
EggWeight *float64 `json:"egg_weight,omitempty"`
|
||||
EggWeightStd float64 `json:"egg_weight_std"`
|
||||
HenHouseAct *float64 `json:"hen_housed_act,omitempty"`
|
||||
HenHouseStd float64 `json:"hen_housed_std"`
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -1,134 +1,103 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
// === CLOSING KEUANGAN CODES ===
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
)
|
||||
// Closing HPP Codes
|
||||
type ClosingHPPCode string
|
||||
|
||||
// === CONSTANTS ===
|
||||
const (
|
||||
HPPGroupPengeluaran = "HPP dan Pengeluaran"
|
||||
HPPGroupBahanBaku = "HPP dan Bahan Baku"
|
||||
HPPLabelOverhead = "Pengeluaran Overhead"
|
||||
HPPLabelEkspedisi = "Beban Ekspedisi"
|
||||
HPPSummaryLabel = "HPP"
|
||||
|
||||
PLSalesTypeChicken = "Penjualan Ayam Besar"
|
||||
PLSalesTypeEgg = "Penjualan Telur"
|
||||
|
||||
PLItemTypeSapronak = "Pembelian Sapronak"
|
||||
PLItemTypeOverhead = "Pengeluaran Overhead"
|
||||
PLItemTypeEkspedisi = "Beban Ekspedisi"
|
||||
|
||||
PLSummaryLabelGrossProfit = "LABA RUGI BRUTTO"
|
||||
PLSummaryLabelSubTotal = "SUB TOTAL"
|
||||
PLSummaryLabelNetProfit = "LABA RUGI NETTO"
|
||||
|
||||
PurchaseLabelPrefix = "Pembelian "
|
||||
HPPCodePakan ClosingHPPCode = "PAKAN"
|
||||
HPPCodeOVK ClosingHPPCode = "OVK"
|
||||
HPPCodeDOC ClosingHPPCode = "DOC"
|
||||
HPPCodeDepresiasi ClosingHPPCode = "DEPRESIASI"
|
||||
HPPCodeOverhead ClosingHPPCode = "OVERHEAD"
|
||||
HPPCodeEkspedisi ClosingHPPCode = "EKSPEDISI"
|
||||
)
|
||||
|
||||
// === CONTEXT STRUCTS ===
|
||||
// Closing Profit Loss Codes
|
||||
type ClosingProfitLossCode string
|
||||
|
||||
type CalculationContext struct {
|
||||
TotalPopulation float64
|
||||
TotalWeightProduced float64
|
||||
TotalEggWeightKg float64
|
||||
TotalDepletion float64
|
||||
TotalWeightSold float64
|
||||
ActualPopulation float64
|
||||
}
|
||||
const (
|
||||
PLCodeSales ClosingProfitLossCode = "SALES"
|
||||
PLCodeSapronak ClosingProfitLossCode = "SAPRONAK"
|
||||
PLCodeOverhead ClosingProfitLossCode = "OVERHEAD"
|
||||
PLCodeEkspedisi ClosingProfitLossCode = "EKSPEDISI"
|
||||
)
|
||||
|
||||
type ClosingKeuanganInput struct {
|
||||
ProjectFlockCategory string
|
||||
PurchaseItems []entities.PurchaseItem
|
||||
Budgets []entities.ProjectBudget
|
||||
Realizations []entities.ExpenseRealization
|
||||
DeliveryProducts []entities.MarketingDeliveryProduct
|
||||
Chickins []entities.ProjectChickin
|
||||
TotalWeightProduced float64
|
||||
TotalEggWeightKg float64
|
||||
TotalDepletion float64
|
||||
}
|
||||
|
||||
// === BASE METRICS ===
|
||||
// === NEW CLOSING KEUANGAN DTO ===
|
||||
|
||||
// FinancialMetrics represents financial metrics with per unit and total amounts
|
||||
type FinancialMetrics struct {
|
||||
RpPerBird float64 `json:"rp_per_bird"`
|
||||
RpPerKg float64 `json:"rp_per_kg"`
|
||||
Amount float64 `json:"amount"`
|
||||
}
|
||||
|
||||
type Comparison struct {
|
||||
// HPPItem represents an item in HPP section
|
||||
type HPPItem struct {
|
||||
ID uint `json:"id"`
|
||||
Category string `json:"category"` // "purchase" or "overhead"
|
||||
Code string `json:"code"` // "PAKAN", "OVK", "DOC", "EKSPEDISI"
|
||||
Label string `json:"label"`
|
||||
Budgeting FinancialMetrics `json:"budgeting"`
|
||||
Realization FinancialMetrics `json:"realization"`
|
||||
}
|
||||
|
||||
// === HPP PURCHASES PACKAGE ===
|
||||
|
||||
type HppItem struct {
|
||||
Type string `json:"type"`
|
||||
Comparison
|
||||
}
|
||||
|
||||
type HppGroup struct {
|
||||
GroupName string `json:"group_name"`
|
||||
Data []HppItem `json:"data"`
|
||||
}
|
||||
|
||||
type SummaryHpp struct {
|
||||
Label string `json:"label"`
|
||||
Comparison `json:"-"`
|
||||
// HPPSummary represents summary for HPP section
|
||||
type HPPSummary struct {
|
||||
Label string `json:"label"`
|
||||
Budgeting FinancialMetrics `json:"budgeting"`
|
||||
Realization FinancialMetrics `json:"realization"`
|
||||
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
|
||||
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
|
||||
}
|
||||
|
||||
type HppPurchasesSection struct {
|
||||
Hpp []HppGroup `json:"hpp"`
|
||||
SummaryHpp SummaryHpp `json:"summary_hpp"`
|
||||
// HPPSection represents HPP data section
|
||||
type HPPSection struct {
|
||||
Items []HPPItem `json:"items"`
|
||||
Summary HPPSummary `json:"summary"`
|
||||
}
|
||||
|
||||
// === PROFIT LOSS PACKAGE ===
|
||||
|
||||
type PLItem struct {
|
||||
Type string `json:"type"`
|
||||
FinancialMetrics
|
||||
// ProfitLossItem represents an item in Profit & Loss section
|
||||
type ProfitLossItem struct {
|
||||
Code string `json:"code"` // "SALES", "PURCHASE_DOC", "OVERHEAD", "EKSPEDISI"
|
||||
Label string `json:"label"`
|
||||
Type string `json:"type"` // "income", "purchase", "overhead"
|
||||
RpPerBird float64 `json:"rp_per_bird"`
|
||||
RpPerKg float64 `json:"rp_per_kg"`
|
||||
Amount float64 `json:"amount"`
|
||||
}
|
||||
|
||||
type PLSummaryItem struct {
|
||||
Label string `json:"label"`
|
||||
FinancialMetrics
|
||||
}
|
||||
|
||||
type PLSummaryGroup struct {
|
||||
GrossProfit PLSummaryItem `json:"gross_profit"`
|
||||
SubTotal PLSummaryItem `json:"sub_total"`
|
||||
NetProfit PLSummaryItem `json:"net_profit"`
|
||||
}
|
||||
|
||||
type ProfitLossData struct {
|
||||
Penjualan []PLItem `json:"penjualan"`
|
||||
Pembelian []PLItem `json:"pembelian"`
|
||||
Overhead PLItem `json:"overhead"`
|
||||
Ekspedisi PLItem `json:"ekspedisi"`
|
||||
Summary PLSummaryGroup `json:"summary"`
|
||||
// ProfitLossSummary represents summary for Profit & Loss section
|
||||
type ProfitLossSummary struct {
|
||||
GrossProfit FinancialMetrics `json:"gross_profit"`
|
||||
SubTotal FinancialMetrics `json:"sub_total"`
|
||||
NetProfit FinancialMetrics `json:"net_profit"`
|
||||
}
|
||||
|
||||
// ProfitLossSection represents Profit & Loss data section
|
||||
type ProfitLossSection struct {
|
||||
Data ProfitLossData `json:"data"`
|
||||
Items []ProfitLossItem `json:"items"`
|
||||
Summary ProfitLossSummary `json:"summary"`
|
||||
}
|
||||
|
||||
// === RESPONSE DTO (ROOT) ===
|
||||
// ClosingKeuanganData represents the main data structure
|
||||
type ClosingKeuanganData struct {
|
||||
HPP HPPSection `json:"hpp"`
|
||||
ProfitLoss ProfitLossSection `json:"profit_loss"`
|
||||
}
|
||||
|
||||
type ReportResponse struct {
|
||||
HppPurchases HppPurchasesSection `json:"hpp_purchases"`
|
||||
ProfitLoss ProfitLossSection `json:"profit_loss"`
|
||||
// ClosingKeuanganResponse represents the full API response
|
||||
type ClosingKeuanganResponse struct {
|
||||
Code int `json:"code"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Data ClosingKeuanganData `json:"data"`
|
||||
}
|
||||
|
||||
// === MAPPER FUNCTIONS ===
|
||||
|
||||
// ToFinancialMetrics creates FinancialMetrics from values
|
||||
func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
|
||||
return FinancialMetrics{
|
||||
RpPerBird: rpPerBird,
|
||||
@@ -137,453 +106,80 @@ func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
|
||||
}
|
||||
}
|
||||
|
||||
func ToComparison(budgeting, realization FinancialMetrics) Comparison {
|
||||
return Comparison{
|
||||
// ToHPPItem creates HPP item
|
||||
func ToHPPItem(id uint, category, code, label string, budgeting, realization FinancialMetrics) HPPItem {
|
||||
return HPPItem{
|
||||
ID: id,
|
||||
Category: category,
|
||||
Code: code,
|
||||
Label: label,
|
||||
Budgeting: budgeting,
|
||||
Realization: realization,
|
||||
}
|
||||
}
|
||||
|
||||
// === HPP PENGELUARAN (from Purchase Items) ===
|
||||
|
||||
func getFlagLabel(flagType utils.FlagType) string {
|
||||
return PurchaseLabelPrefix + string(flagType)
|
||||
}
|
||||
|
||||
func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, ctx CalculationContext) []HppItem {
|
||||
flags := []utils.FlagType{
|
||||
utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagPakan,
|
||||
utils.FlagPreStarter, utils.FlagStarter, utils.FlagFinisher,
|
||||
utils.FlagOVK, utils.FlagObat, utils.FlagVitamin, utils.FlagKimia,
|
||||
}
|
||||
|
||||
items := []HppItem{}
|
||||
seenFlags := make(map[utils.FlagType]bool)
|
||||
|
||||
for _, item := range purchaseItems {
|
||||
if item.Product == nil || len(item.Product.Flags) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, flag := range item.Product.Flags {
|
||||
flagType := utils.FlagType(flag.Name)
|
||||
|
||||
if slices.Contains(flags, flagType) && !seenFlags[flagType] {
|
||||
amount := sumPurchasesByFlag(purchaseItems, flagType)
|
||||
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
||||
|
||||
items = append(items, HppItem{
|
||||
Type: getFlagLabel(flagType),
|
||||
Comparison: ToComparison(
|
||||
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
|
||||
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
|
||||
),
|
||||
})
|
||||
seenFlags[flagType] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
// === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) ===
|
||||
|
||||
func createHppOverheadItem(budgetAmount, realizationAmount float64, ctx CalculationContext) HppItem {
|
||||
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
||||
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
||||
|
||||
return HppItem{
|
||||
Type: HPPLabelOverhead,
|
||||
Comparison: ToComparison(
|
||||
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount),
|
||||
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount),
|
||||
),
|
||||
// ToHPPSummary creates HPP summary
|
||||
func ToHPPSummary(label string, budgeting, realization FinancialMetrics, eggBudgeting, eggRealization *FinancialMetrics) HPPSummary {
|
||||
return HPPSummary{
|
||||
Label: label,
|
||||
Budgeting: budgeting,
|
||||
Realization: realization,
|
||||
EggBudgeting: eggBudgeting,
|
||||
EggRealization: eggRealization,
|
||||
}
|
||||
}
|
||||
|
||||
func createHppEkspedisiItem(ekspedisiAmount float64, ctx CalculationContext) HppItem {
|
||||
ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
||||
|
||||
return HppItem{
|
||||
Type: HPPLabelEkspedisi,
|
||||
Comparison: ToComparison(
|
||||
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
|
||||
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
|
||||
),
|
||||
// ToHPPSection creates HPP section
|
||||
func ToHPPSection(items []HPPItem, summary HPPSummary) HPPSection {
|
||||
return HPPSection{
|
||||
Items: items,
|
||||
Summary: summary,
|
||||
}
|
||||
}
|
||||
|
||||
func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppGroup {
|
||||
items := []HppItem{}
|
||||
|
||||
budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
|
||||
realizationAmount := getOperationalExpenses(realizations)
|
||||
|
||||
if budgetAmount > 0 || realizationAmount > 0 {
|
||||
items = append(items, createHppOverheadItem(budgetAmount, realizationAmount, ctx))
|
||||
}
|
||||
|
||||
ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
|
||||
items = append(items, createHppEkspedisiItem(ekspedisiAmount, ctx))
|
||||
|
||||
return HppGroup{
|
||||
GroupName: HPPGroupBahanBaku,
|
||||
Data: items,
|
||||
// ToProfitLossItem creates Profit & Loss item
|
||||
func ToProfitLossItem(code, label, itemType string, rpPerBird, rpPerKg, amount float64) ProfitLossItem {
|
||||
return ProfitLossItem{
|
||||
Code: code,
|
||||
Label: label,
|
||||
Type: itemType,
|
||||
RpPerBird: rpPerBird,
|
||||
RpPerKg: rpPerKg,
|
||||
Amount: amount,
|
||||
}
|
||||
}
|
||||
|
||||
// === HPP SUMMARY ===
|
||||
|
||||
func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, projectFlockCategory string, ctx CalculationContext) SummaryHpp {
|
||||
purchaseTotal := sumPurchaseTotal(purchaseItems)
|
||||
budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
|
||||
totalBudget := purchaseTotal + budgetTotal
|
||||
|
||||
totalRealization := sumRealizationsByFilter(realizations, func(*entities.ExpenseRealization) bool { return true })
|
||||
|
||||
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
||||
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
||||
|
||||
summary := SummaryHpp{
|
||||
Label: label,
|
||||
Comparison: ToComparison(
|
||||
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
|
||||
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
|
||||
),
|
||||
}
|
||||
|
||||
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) && ctx.TotalEggWeightKg > 0 {
|
||||
budgetEggRpPerKg, _ := calculatePerUnitMetrics(totalBudget, 0, ctx.TotalEggWeightKg)
|
||||
realizationEggRpPerKg, _ := calculatePerUnitMetrics(totalRealization, 0, ctx.TotalEggWeightKg)
|
||||
|
||||
summary.EggBudgeting = &FinancialMetrics{
|
||||
RpPerBird: 0,
|
||||
RpPerKg: budgetEggRpPerKg,
|
||||
Amount: totalBudget,
|
||||
}
|
||||
summary.EggRealization = &FinancialMetrics{
|
||||
RpPerBird: 0,
|
||||
RpPerKg: realizationEggRpPerKg,
|
||||
Amount: totalRealization,
|
||||
}
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, projectFlockCategory string, ctx CalculationContext) HppPurchasesSection {
|
||||
hppGroups := []HppGroup{
|
||||
{
|
||||
GroupName: HPPGroupPengeluaran,
|
||||
Data: buildHppItemsByPurchaseFlags(purchaseItems, ctx),
|
||||
},
|
||||
ToHppBahanBakuGroup(budgets, realizations, ctx),
|
||||
}
|
||||
|
||||
summaryHpp := ToSummaryHpp(HPPSummaryLabel, purchaseItems, budgets, realizations, projectFlockCategory, ctx)
|
||||
|
||||
return HppPurchasesSection{
|
||||
Hpp: hppGroups,
|
||||
SummaryHpp: summaryHpp,
|
||||
// ToProfitLossSummary creates Profit & Loss summary
|
||||
func ToProfitLossSummary(grossProfit, subTotal, netProfit FinancialMetrics) ProfitLossSummary {
|
||||
return ProfitLossSummary{
|
||||
GrossProfit: grossProfit,
|
||||
SubTotal: subTotal,
|
||||
NetProfit: netProfit,
|
||||
}
|
||||
}
|
||||
|
||||
// === PROFIT & LOSS ===
|
||||
|
||||
func ToPLItem(itemType string, metrics FinancialMetrics) PLItem {
|
||||
return PLItem{
|
||||
Type: itemType,
|
||||
FinancialMetrics: metrics,
|
||||
}
|
||||
}
|
||||
|
||||
func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem {
|
||||
return PLSummaryItem{
|
||||
Label: label,
|
||||
FinancialMetrics: metrics,
|
||||
}
|
||||
}
|
||||
|
||||
func createPLItemWithMetrics(itemType string, amount float64, ctx CalculationContext) PLItem {
|
||||
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightProduced)
|
||||
return ToPLItem(itemType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))
|
||||
}
|
||||
|
||||
func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) {
|
||||
for _, item := range items {
|
||||
totalAmount += item.Amount
|
||||
totalPerBird += item.RpPerBird
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func createPenjualanItem(salesType string, amount float64, ctx CalculationContext) PLItem {
|
||||
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightSold)
|
||||
return ToPLItem(salesType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))
|
||||
}
|
||||
|
||||
func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, ctx CalculationContext) []PLItem {
|
||||
items := []PLItem{}
|
||||
|
||||
categorized := categorizeDeliveriesBySalesType(deliveryProducts)
|
||||
|
||||
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) {
|
||||
ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken])
|
||||
telurAmount := sumDeliveriesByCategory(categorized[PLSalesTypeEgg])
|
||||
|
||||
items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx))
|
||||
items = append(items, createPenjualanItem(PLSalesTypeEgg, telurAmount, ctx))
|
||||
} else {
|
||||
ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken])
|
||||
items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx))
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func ToPembelianItems(purchases []entities.PurchaseItem, realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
|
||||
purchaseAmount := sumPurchaseTotal(purchases)
|
||||
|
||||
return []PLItem{
|
||||
createPLItemWithMetrics(PLItemTypeSapronak, purchaseAmount, ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func ToOverheadItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
|
||||
realizationAmount := getOperationalExpenses(realizations)
|
||||
return []PLItem{
|
||||
createPLItemWithMetrics(PLItemTypeOverhead, realizationAmount, ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func ToEkspedisiItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
|
||||
amount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
|
||||
return []PLItem{
|
||||
createPLItemWithMetrics(PLItemTypeEkspedisi, amount, ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) PLSummaryGroup {
|
||||
totalPenjualan, totalPenjualanPerBird := sumPLItems(penjualanItems)
|
||||
totalPembelian, totalPembelianPerBird := sumPLItems(pembelianItems)
|
||||
totalOverhead, totalOverheadPerBird := sumPLItems(overheadItems)
|
||||
totalEkspedisi, totalEkspedisiPerBird := sumPLItems(ekspedisiItems)
|
||||
|
||||
grossProfit := totalPenjualan - totalPembelian
|
||||
grossProfitPerBird := totalPenjualanPerBird - totalPembelianPerBird
|
||||
|
||||
totalOtherExpenses := totalOverhead + totalEkspedisi
|
||||
totalOtherExpensesPerBird := totalOverheadPerBird + totalEkspedisiPerBird
|
||||
|
||||
netProfit := grossProfit - totalOtherExpenses
|
||||
netProfitPerBird := grossProfitPerBird - totalOtherExpensesPerBird
|
||||
|
||||
return PLSummaryGroup{
|
||||
GrossProfit: ToPLSummaryItem(PLSummaryLabelGrossProfit, ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)),
|
||||
SubTotal: ToPLSummaryItem(PLSummaryLabelSubTotal, ToFinancialMetrics(totalOtherExpensesPerBird, 0, totalOtherExpenses)),
|
||||
NetProfit: ToPLSummaryItem(PLSummaryLabelNetProfit, ToFinancialMetrics(netProfitPerBird, 0, netProfit)),
|
||||
}
|
||||
}
|
||||
|
||||
func ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossData {
|
||||
summary := ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems)
|
||||
|
||||
totalOverhead := aggregatePLItems(overheadItems, PLItemTypeOverhead)
|
||||
totalEkspedisi := aggregatePLItems(ekspedisiItems, PLItemTypeEkspedisi)
|
||||
|
||||
return ProfitLossData{
|
||||
Penjualan: penjualanItems,
|
||||
Pembelian: pembelianItems,
|
||||
Overhead: totalOverhead,
|
||||
Ekspedisi: totalEkspedisi,
|
||||
Summary: summary,
|
||||
}
|
||||
}
|
||||
|
||||
func ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossSection {
|
||||
// ToProfitLossSection creates Profit & Loss section
|
||||
func ToProfitLossSection(items []ProfitLossItem, summary ProfitLossSummary) ProfitLossSection {
|
||||
return ProfitLossSection{
|
||||
Data: ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems),
|
||||
Items: items,
|
||||
Summary: summary,
|
||||
}
|
||||
}
|
||||
|
||||
func aggregatePLItems(items []PLItem, label string) PLItem {
|
||||
totalAmount, totalPerBird := sumPLItems(items)
|
||||
return ToPLItem(label, ToFinancialMetrics(totalPerBird, 0, totalAmount))
|
||||
}
|
||||
|
||||
func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse {
|
||||
return ReportResponse{
|
||||
HppPurchases: hppPurchases,
|
||||
ProfitLoss: profitLoss,
|
||||
// ToClosingKeuanganData creates complete closing keuangan data
|
||||
func ToClosingKeuanganData(hpp HPPSection, profitLoss ProfitLossSection) ClosingKeuanganData {
|
||||
return ClosingKeuanganData{
|
||||
HPP: hpp,
|
||||
ProfitLoss: profitLoss,
|
||||
}
|
||||
}
|
||||
|
||||
func ToClosingKeuanganReport(input ClosingKeuanganInput) ReportResponse {
|
||||
var totalPopulation float64
|
||||
var totalWeightSold float64
|
||||
|
||||
for _, chickin := range input.Chickins {
|
||||
totalPopulation += chickin.UsageQty
|
||||
}
|
||||
|
||||
for _, delivery := range input.DeliveryProducts {
|
||||
totalWeightSold += delivery.TotalWeight
|
||||
}
|
||||
|
||||
ctx := CalculationContext{
|
||||
TotalPopulation: totalPopulation,
|
||||
TotalWeightProduced: input.TotalWeightProduced,
|
||||
TotalEggWeightKg: input.TotalEggWeightKg,
|
||||
TotalDepletion: input.TotalDepletion,
|
||||
TotalWeightSold: totalWeightSold,
|
||||
ActualPopulation: totalPopulation - input.TotalDepletion,
|
||||
}
|
||||
|
||||
hppSection := ToHppPurchasesSection(input.PurchaseItems, input.Budgets, input.Realizations, input.ProjectFlockCategory, ctx)
|
||||
penjualanItems := ToPenjualanItems(input.ProjectFlockCategory, input.DeliveryProducts, ctx)
|
||||
pembelianItems := ToPembelianItems(input.PurchaseItems, input.Realizations, ctx)
|
||||
overheadItems := ToOverheadItems(input.Realizations, ctx)
|
||||
ekspedisiItems := ToEkspedisiItems(input.Realizations, ctx)
|
||||
plSection := ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems)
|
||||
|
||||
return ToReportResponse(hppSection, plSection)
|
||||
}
|
||||
|
||||
// === HELPER FUNCTIONS ===
|
||||
|
||||
func calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold float64) (rpPerBird, rpPerKg float64) {
|
||||
if totalPopulation > 0 {
|
||||
rpPerBird = amount / totalPopulation
|
||||
}
|
||||
if totalWeightSold > 0 {
|
||||
rpPerKg = amount / totalWeightSold
|
||||
}
|
||||
return rpPerBird, rpPerKg
|
||||
}
|
||||
|
||||
func hasProductFlag(flags []entities.Flag, flagType utils.FlagType) bool {
|
||||
for _, flag := range flags {
|
||||
if strings.ToUpper(flag.Name) == string(flagType) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func filterByPurchaseFlag(flagType utils.FlagType) func(*entities.PurchaseItem) bool {
|
||||
return func(item *entities.PurchaseItem) bool {
|
||||
if item.Product == nil || len(item.Product.Flags) == 0 {
|
||||
return false
|
||||
}
|
||||
return hasProductFlag(item.Product.Flags, flagType)
|
||||
// ToSuccessClosingKeuanganResponse creates success response
|
||||
func ToSuccessClosingKeuanganResponse(data ClosingKeuanganData) ClosingKeuanganResponse {
|
||||
return ClosingKeuanganResponse{
|
||||
Code: 200,
|
||||
Status: "success",
|
||||
Message: "Get closing keuangan successfully",
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func filterRealizationByNonstockFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool {
|
||||
return func(realization *entities.ExpenseRealization) bool {
|
||||
if realization.ExpenseNonstock == nil || realization.ExpenseNonstock.Nonstock == nil {
|
||||
return false
|
||||
}
|
||||
return hasProductFlag(realization.ExpenseNonstock.Nonstock.Flags, flagType)
|
||||
}
|
||||
}
|
||||
|
||||
func filterRealizationExceptFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool {
|
||||
hasFlag := filterRealizationByNonstockFlag(flagType)
|
||||
return func(realization *entities.ExpenseRealization) bool {
|
||||
return !hasFlag(realization)
|
||||
}
|
||||
}
|
||||
|
||||
func sumByFilter[T any](items []T, extractor func(*T) float64, filter func(*T) bool) float64 {
|
||||
amount := 0.0
|
||||
for i := range items {
|
||||
if filter(&items[i]) {
|
||||
amount += extractor(&items[i])
|
||||
}
|
||||
}
|
||||
return amount
|
||||
}
|
||||
|
||||
func sumPurchasesByFilter(purchases []entities.PurchaseItem, filter func(*entities.PurchaseItem) bool) float64 {
|
||||
return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, filter)
|
||||
}
|
||||
|
||||
func sumPurchasesByFlag(purchases []entities.PurchaseItem, flagType utils.FlagType) float64 {
|
||||
return sumPurchasesByFilter(purchases, filterByPurchaseFlag(flagType))
|
||||
}
|
||||
|
||||
func sumPurchaseTotal(purchases []entities.PurchaseItem) float64 {
|
||||
return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, func(*entities.PurchaseItem) bool { return true })
|
||||
}
|
||||
|
||||
func sumBudgetsByFilter(budgets []entities.ProjectBudget, filter func(*entities.ProjectBudget) bool) float64 {
|
||||
return sumByFilter(budgets, func(b *entities.ProjectBudget) float64 { return b.Price * b.Qty }, filter)
|
||||
}
|
||||
|
||||
func sumRealizationsByFilter(realizations []entities.ExpenseRealization, filter func(*entities.ExpenseRealization) bool) float64 {
|
||||
return sumByFilter(realizations, func(r *entities.ExpenseRealization) float64 { return r.Price * r.Qty }, filter)
|
||||
}
|
||||
|
||||
func getOperationalExpenses(realizations []entities.ExpenseRealization) float64 {
|
||||
return sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi))
|
||||
}
|
||||
|
||||
func isChickenProductFlag(flagType utils.FlagType) bool {
|
||||
switch flagType {
|
||||
case utils.FlagDOC, utils.FlagPullet, utils.FlagLayer,
|
||||
utils.FlagAyamAfkir, utils.FlagAyamCulling, utils.FlagAyamMati:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isEggProductFlag(flagType utils.FlagType) bool {
|
||||
switch flagType {
|
||||
case utils.FlagTelur, utils.FlagTelurUtuh, utils.FlagTelurPecah,
|
||||
utils.FlagTelurPutih, utils.FlagTelurRetak:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getSalesTypeFromProductFlags(product *entities.Product) string {
|
||||
if product == nil || len(product.Flags) == 0 {
|
||||
return PLSalesTypeChicken
|
||||
}
|
||||
|
||||
for _, flag := range product.Flags {
|
||||
flagType := utils.FlagType(strings.ToUpper(flag.Name))
|
||||
|
||||
if isEggProductFlag(flagType) {
|
||||
return PLSalesTypeEgg
|
||||
}
|
||||
if isChickenProductFlag(flagType) {
|
||||
return PLSalesTypeChicken
|
||||
}
|
||||
}
|
||||
|
||||
return PLSalesTypeChicken
|
||||
}
|
||||
|
||||
func categorizeDeliveriesBySalesType(deliveries []entities.MarketingDeliveryProduct) map[string][]entities.MarketingDeliveryProduct {
|
||||
categorized := make(map[string][]entities.MarketingDeliveryProduct)
|
||||
|
||||
for _, delivery := range deliveries {
|
||||
product := delivery.MarketingProduct.ProductWarehouse.Product
|
||||
salesType := getSalesTypeFromProductFlags(&product)
|
||||
|
||||
categorized[salesType] = append(categorized[salesType], delivery)
|
||||
}
|
||||
|
||||
return categorized
|
||||
}
|
||||
|
||||
func sumDeliveriesByCategory(deliveries []entities.MarketingDeliveryProduct) float64 {
|
||||
amount := 0.0
|
||||
for _, delivery := range deliveries {
|
||||
amount += delivery.TotalPrice
|
||||
}
|
||||
return amount
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -114,6 +114,17 @@ type ClosingSapronakDTO struct {
|
||||
OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"`
|
||||
}
|
||||
|
||||
type ClosingSapronakSummaryItemDTO struct {
|
||||
Category string `json:"category"`
|
||||
TotalQty int64 `json:"total_qty"`
|
||||
Uom UomSummaryDTO `json:"uom"`
|
||||
}
|
||||
|
||||
type UomSummaryDTO struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// === Mapper Functions for Aggregated Sapronak Response ===
|
||||
|
||||
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
|
||||
@@ -201,18 +212,48 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
|
||||
switch strings.ToLower(item.JenisTransaksi) {
|
||||
case "pembelian", "adjustment masuk", "mutasi masuk":
|
||||
row.QtyIn += item.QtyMasuk
|
||||
row.TotalAmount += item.Nilai
|
||||
if row.UnitPrice == 0 {
|
||||
if item.QtyMasuk > 0 && item.Nilai > 0 {
|
||||
row.UnitPrice = item.Nilai / item.QtyMasuk
|
||||
} else if item.Harga > 0 {
|
||||
row.UnitPrice = item.Harga
|
||||
}
|
||||
}
|
||||
if strings.ToLower(item.JenisTransaksi) == "mutasi masuk" {
|
||||
ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi))
|
||||
if strings.HasPrefix(ref, "TL-") {
|
||||
row.Notes = "TRANSFER LAYING"
|
||||
} else if strings.HasPrefix(ref, "ST-") {
|
||||
row.Notes = "TRANSFER STOCK"
|
||||
}
|
||||
}
|
||||
case "pemakaian", "adjustment keluar":
|
||||
price := row.UnitPrice
|
||||
if price == 0 {
|
||||
price = item.Harga
|
||||
}
|
||||
row.QtyUsed += item.QtyKeluar
|
||||
case "mutasi keluar":
|
||||
row.TotalAmount += item.QtyKeluar * price
|
||||
case "mutasi keluar", "penjualan":
|
||||
price := row.UnitPrice
|
||||
if price == 0 {
|
||||
price = item.Harga
|
||||
}
|
||||
row.QtyOut += item.QtyKeluar
|
||||
if strings.ToLower(item.JenisTransaksi) == "mutasi keluar" {
|
||||
ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi))
|
||||
if strings.HasPrefix(ref, "TL-") {
|
||||
row.Notes = "TRANSFER LAYING"
|
||||
} else if strings.HasPrefix(ref, "ST-") {
|
||||
row.Notes = "TRANSFER STOCK"
|
||||
}
|
||||
}
|
||||
default:
|
||||
row.QtyIn += item.QtyMasuk
|
||||
row.TotalAmount += item.Nilai
|
||||
}
|
||||
|
||||
if row.QtyIn > 0 {
|
||||
row.UnitPrice = row.TotalAmount / row.QtyIn
|
||||
if row.QtyIn > 0 {
|
||||
row.UnitPrice = row.TotalAmount / row.QtyIn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,8 +274,8 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
|
||||
total += r.TotalAmount
|
||||
}
|
||||
avg := 0.0
|
||||
if qtyIn > 0 {
|
||||
avg = total / qtyIn
|
||||
if qtyUsed > 0 {
|
||||
avg = total / qtyUsed
|
||||
}
|
||||
cat.Total = SapronakCategoryTotalDTO{
|
||||
Label: label,
|
||||
|
||||
@@ -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"
|
||||
@@ -24,6 +25,7 @@ type ClosingModule struct{}
|
||||
|
||||
func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
closingRepo := rClosing.NewClosingRepository(db)
|
||||
closingKeuanganRepo := rClosing.NewClosingKeuanganRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
||||
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
|
||||
@@ -33,13 +35,16 @@ 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)
|
||||
closingKeuanganService := sClosing.NewClosingKeuanganService(closingKeuanganRepo, projectFlockRepo, projectFlockKandangRepo, marketingDeliveryProductRepo, expenseRealizationRepo, projectBudgetRepo, chickinRepo, recordingRepo)
|
||||
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
ClosingRoutes(router, userService, closingService, sapronakService)
|
||||
ClosingRoutes(router, userService, closingService, sapronakService, closingKeuanganService)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ import (
|
||||
type ClosingRepository interface {
|
||||
repository.BaseRepository[entity.ProjectFlock]
|
||||
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
|
||||
GetSapronakSummary(ctx context.Context, params SapronakQueryParams) ([]SapronakSummaryRow, 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)
|
||||
@@ -32,7 +34,7 @@ type ClosingRepository interface {
|
||||
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||
GetActualUsageCostByProjectFlockID(ctx context.Context, projectFlockID uint) ([]ActualUsageCostRow, error)
|
||||
FetchSapronakSales(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
|
||||
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
|
||||
}
|
||||
|
||||
@@ -59,10 +61,18 @@ type SapronakRow struct {
|
||||
DestinationWarehouse string `gorm:"column:destination_warehouse"`
|
||||
Destination string `gorm:"column:destination"`
|
||||
Quantity float64 `gorm:"column:quantity"`
|
||||
UnitID uint `gorm:"column:unit_id"`
|
||||
Unit string `gorm:"column:unit"`
|
||||
Notes string `gorm:"column:notes"`
|
||||
}
|
||||
|
||||
type SapronakSummaryRow struct {
|
||||
Category string `gorm:"column:category"`
|
||||
TotalQty int64 `gorm:"column:total_qty"`
|
||||
UomID uint `gorm:"column:uom_id"`
|
||||
UomName string `gorm:"column:uom_name"`
|
||||
}
|
||||
|
||||
type ExpeditionHPPRow struct {
|
||||
SupplierName string `gorm:"column:supplier_name"`
|
||||
TotalAmount float64 `gorm:"column:total_amount"`
|
||||
@@ -74,6 +84,7 @@ type SapronakQueryParams struct {
|
||||
ProjectFlockKandangIDs []uint
|
||||
Limit int
|
||||
Offset int
|
||||
Search string
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) {
|
||||
@@ -109,14 +120,36 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
|
||||
|
||||
unionSQL := strings.Join(unionParts, " UNION ALL ")
|
||||
|
||||
search := strings.TrimSpace(params.Search)
|
||||
searchClause := ""
|
||||
var searchArgs []any
|
||||
if search != "" {
|
||||
searchClause = `
|
||||
WHERE (
|
||||
reference_number ILIKE ?
|
||||
OR product_name ILIKE ?
|
||||
OR product_category ILIKE ?
|
||||
OR source_warehouse ILIKE ?
|
||||
OR destination_warehouse ILIKE ?
|
||||
OR CAST(quantity AS TEXT) ILIKE ?
|
||||
OR unit ILIKE ?
|
||||
OR notes ILIKE ?
|
||||
OR transaction_type ILIKE ?
|
||||
)`
|
||||
like := "%" + search + "%"
|
||||
searchArgs = append(searchArgs, like, like, like, like, like, like, like, like, like)
|
||||
}
|
||||
|
||||
var totalResults int64
|
||||
countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined", unionSQL)
|
||||
if err := db.Raw(countSQL, args...).Scan(&totalResults).Error; err != nil {
|
||||
countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined%s", unionSQL, searchClause)
|
||||
countArgs := append(append([]any{}, args...), searchArgs...)
|
||||
if err := db.Raw(countSQL, countArgs...).Scan(&totalResults).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
dataArgs := append(append([]any{}, args...), params.Limit, params.Offset)
|
||||
dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL)
|
||||
dataArgs := append(append([]any{}, args...), searchArgs...)
|
||||
dataArgs = append(dataArgs, params.Limit, params.Offset)
|
||||
dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined%s ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL, searchClause)
|
||||
|
||||
var rows []SapronakRow
|
||||
if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil {
|
||||
@@ -126,6 +159,79 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
|
||||
return rows, totalResults, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) GetSapronakSummary(ctx context.Context, params SapronakQueryParams) ([]SapronakSummaryRow, error) {
|
||||
db := r.DB().WithContext(ctx)
|
||||
|
||||
var (
|
||||
unionParts []string
|
||||
args []any
|
||||
)
|
||||
|
||||
switch params.Type {
|
||||
case validation.SapronakTypeIncoming:
|
||||
if len(params.WarehouseIDs) == 0 {
|
||||
return []SapronakSummaryRow{}, nil
|
||||
}
|
||||
unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL)
|
||||
args = append(args, params.WarehouseIDs, params.WarehouseIDs)
|
||||
case validation.SapronakTypeOutgoing:
|
||||
if len(params.WarehouseIDs) > 0 {
|
||||
unionParts = append(unionParts, sapronakOutgoingTransfersSQL)
|
||||
args = append(args, params.WarehouseIDs)
|
||||
}
|
||||
if len(params.ProjectFlockKandangIDs) > 0 {
|
||||
unionParts = append(unionParts, sapronakOutgoingMarketingsSQL)
|
||||
args = append(args, params.ProjectFlockKandangIDs)
|
||||
}
|
||||
if len(unionParts) == 0 {
|
||||
return []SapronakSummaryRow{}, nil
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid sapronak type: %s", params.Type)
|
||||
}
|
||||
|
||||
unionSQL := strings.Join(unionParts, " UNION ALL ")
|
||||
|
||||
search := strings.TrimSpace(params.Search)
|
||||
searchClause := ""
|
||||
var searchArgs []any
|
||||
if search != "" {
|
||||
searchClause = `
|
||||
WHERE (
|
||||
reference_number ILIKE ?
|
||||
OR product_name ILIKE ?
|
||||
OR product_category ILIKE ?
|
||||
OR source_warehouse ILIKE ?
|
||||
OR destination_warehouse ILIKE ?
|
||||
OR CAST(quantity AS TEXT) ILIKE ?
|
||||
OR unit ILIKE ?
|
||||
OR notes ILIKE ?
|
||||
OR transaction_type ILIKE ?
|
||||
)`
|
||||
like := "%" + search + "%"
|
||||
searchArgs = append(searchArgs, like, like, like, like, like, like, like, like, like)
|
||||
}
|
||||
|
||||
querySQL := fmt.Sprintf(`
|
||||
SELECT
|
||||
product_category AS category,
|
||||
CAST(COALESCE(SUM(quantity), 0) AS BIGINT) AS total_qty,
|
||||
unit_id AS uom_id,
|
||||
unit AS uom_name
|
||||
FROM (%s) AS combined%s
|
||||
GROUP BY product_category, unit_id, unit
|
||||
ORDER BY product_category ASC, unit ASC
|
||||
`, unionSQL, searchClause)
|
||||
queryArgs := append(append([]any{}, args...), searchArgs...)
|
||||
|
||||
var rows []SapronakSummaryRow
|
||||
if err := db.Raw(querySQL, queryArgs...).Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) {
|
||||
if len(projectFlockKandangIDs) == 0 {
|
||||
return 0, 0, nil
|
||||
@@ -166,6 +272,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
|
||||
@@ -300,6 +423,7 @@ func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlo
|
||||
Joins("JOIN suppliers s ON s.id = e.supplier_id").
|
||||
Where("pfk.project_flock_id = ?", projectFlockID).
|
||||
Where("e.category = ?", "BOP").
|
||||
Where("e.realization_date IS NOT NULL").
|
||||
Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi)))
|
||||
|
||||
if projectFlockKandangID != nil && *projectFlockKandangID != 0 {
|
||||
@@ -361,6 +485,7 @@ SELECT
|
||||
w.name AS destination_warehouse,
|
||||
'' AS destination,
|
||||
pi.total_qty AS quantity,
|
||||
u.id AS unit_id,
|
||||
u.name AS unit,
|
||||
COALESCE(p.notes, '') AS notes
|
||||
FROM purchase_items pi
|
||||
@@ -409,6 +534,7 @@ SELECT
|
||||
COALESCE(tw.name, '') AS destination_warehouse,
|
||||
'' AS destination,
|
||||
std.usage_qty AS quantity,
|
||||
u.id AS unit_id,
|
||||
u.name AS unit,
|
||||
'Stock Refill' AS notes
|
||||
FROM stock_transfer_details std
|
||||
@@ -455,9 +581,10 @@ SELECT
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_sub_category,
|
||||
COALESCE(fw.name, '') AS source_warehouse,
|
||||
'' AS destination_warehouse,
|
||||
COALESCE(tw.name, '') AS destination,
|
||||
COALESCE(tw.name, '') AS destination_warehouse,
|
||||
'' AS destination,
|
||||
std.usage_qty AS quantity,
|
||||
u.id AS unit_id,
|
||||
u.name AS unit,
|
||||
'Transfer to other unit' AS notes
|
||||
FROM stock_transfer_details std
|
||||
@@ -504,18 +631,27 @@ SELECT
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_sub_category,
|
||||
w.name AS source_warehouse,
|
||||
'' AS destination_warehouse,
|
||||
'RETAIL CUSTOMER' AS destination,
|
||||
COALESCE(c.name, '') AS destination_warehouse,
|
||||
'' AS destination,
|
||||
mp.qty AS quantity,
|
||||
u.id AS unit_id,
|
||||
u.name AS unit,
|
||||
m.notes AS notes
|
||||
FROM marketing_products mp
|
||||
JOIN marketings m ON m.id = mp.marketing_id
|
||||
LEFT JOIN customers c ON c.id = m.customer_id
|
||||
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
||||
JOIN products prod ON prod.id = pw.product_id
|
||||
JOIN uoms u ON u.id = prod.uom_id
|
||||
JOIN warehouses w ON w.id = pw.warehouse_id
|
||||
WHERE pw.project_flock_kandang_id IN ?
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM flags f
|
||||
WHERE f.flagable_id = pw.product_id
|
||||
AND f.flagable_type = 'products'
|
||||
AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET')
|
||||
)
|
||||
`
|
||||
)
|
||||
|
||||
@@ -871,143 +1007,156 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||
rows, err := r.fetchStockLogs(ctx, kandangID, string(utils.StockLogTypeTransfer), true)
|
||||
incomingQuery := r.withCtx(ctx).
|
||||
Table("stock_transfer_details AS std").
|
||||
Select(`
|
||||
std.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
st.transfer_date::timestamp AS date,
|
||||
COALESCE(st.movement_number, '') AS reference,
|
||||
COALESCE(std.total_qty, 0) AS qty_in,
|
||||
0 AS qty_out,
|
||||
COALESCE(p.product_price, 0) AS price
|
||||
`).
|
||||
Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id").
|
||||
Joins("LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = std.dest_product_warehouse_id").
|
||||
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
||||
Joins("JOIN products p ON p.id = std.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Where("w.kandang_id = ?", kandangID).
|
||||
Where("(fw.kandang_id IS NULL OR fw.kandang_id <> w.kandang_id)").
|
||||
Where("f.name IN ?", sapronakFlagsAll)
|
||||
incoming, err := scanAndGroupDetails(incomingQuery)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string {
|
||||
if ref := strings.TrimSpace(row.MovementNumber); ref != "" {
|
||||
return ref
|
||||
}
|
||||
return fmt.Sprintf("TRF-%d", row.ID)
|
||||
})
|
||||
return in, out, nil
|
||||
}
|
||||
|
||||
type ActualUsageCostRow struct {
|
||||
ProductID uint `gorm:"column:product_id"`
|
||||
ProductName string `gorm:"column:product_name"`
|
||||
FlagName string `gorm:"column:flag_name"`
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
TotalPrice float64 `gorm:"column:total_price"`
|
||||
AveragePrice float64 `gorm:"column:average_price"`
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.Context, projectFlockID uint) ([]ActualUsageCostRow, error) {
|
||||
if projectFlockID == 0 {
|
||||
return []ActualUsageCostRow{}, nil
|
||||
}
|
||||
|
||||
db := r.DB().WithContext(ctx)
|
||||
|
||||
// Get all project flock kandang IDs for this project flock
|
||||
var pfkIDs []uint
|
||||
err := db.Table("project_flock_kandangs").
|
||||
Where("project_flock_id = ?", projectFlockID).
|
||||
Pluck("id", &pfkIDs).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(pfkIDs) == 0 {
|
||||
return []ActualUsageCostRow{}, nil
|
||||
}
|
||||
|
||||
var rows []ActualUsageCostRow
|
||||
|
||||
purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
|
||||
transferStockableKey := fifo.StockableKeyStockTransferIn.String()
|
||||
|
||||
recordingQuery := db.
|
||||
Table("recordings AS r").
|
||||
incomingLayingQuery := r.withCtx(ctx).
|
||||
Table("laying_transfer_targets AS ltt").
|
||||
Select(`
|
||||
pw.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
COALESCE(f.name, tf.name) AS flag_name,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
|
||||
ELSE 0
|
||||
END
|
||||
), 0) AS total_qty,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0)
|
||||
ELSE 0
|
||||
END
|
||||
), 0) AS total_price,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
|
||||
ELSE 0
|
||||
END
|
||||
), 0) AS qty_divisor,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0)
|
||||
ELSE 0
|
||||
END
|
||||
), 0) / NULLIF(COALESCE(SUM(
|
||||
CASE
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
|
||||
ELSE 0
|
||||
END
|
||||
), 0), 0) AS average_price`,
|
||||
purchaseStockableKey, transferStockableKey,
|
||||
purchaseStockableKey, transferStockableKey,
|
||||
purchaseStockableKey, transferStockableKey,
|
||||
purchaseStockableKey, transferStockableKey,
|
||||
purchaseStockableKey, transferStockableKey).
|
||||
Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.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("LEFT JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.status = ?",
|
||||
"recording_stocks", entity.StockAllocationStatusActive).
|
||||
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", purchaseStockableKey).
|
||||
Joins("LEFT JOIN stock_transfer_details AS std ON std.id = sa.stockable_id AND sa.stockable_type = ?", transferStockableKey).
|
||||
Joins("LEFT JOIN stock_transfers AS st ON st.id = std.stock_transfer_id").
|
||||
Joins("LEFT JOIN purchase_items AS tpi ON tpi.product_id = std.product_id AND tpi.warehouse_id = st.from_warehouse_id").
|
||||
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Joins("LEFT JOIN flags AS tf ON tf.flagable_id = std.product_id AND tf.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Where("r.project_flock_kandangs_id IN ?", pfkIDs).
|
||||
Where("r.deleted_at IS NULL").
|
||||
Group("pw.product_id, p.name, COALESCE(f.name, tf.name)")
|
||||
|
||||
if err := recordingQuery.Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chickinQuery := db.
|
||||
Table("project_chickins AS pc").
|
||||
Select(`
|
||||
pw.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag_name,
|
||||
COALESCE(SUM(pc.usage_qty), 0) AS total_qty,
|
||||
COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS total_price,
|
||||
COALESCE(AVG(COALESCE(pi.price, 0)), 0) AS average_price
|
||||
f.name AS flag,
|
||||
lt.transfer_date::timestamp AS date,
|
||||
COALESCE(lt.transfer_number, '') AS reference,
|
||||
COALESCE(ltt.total_qty, 0) AS qty_in,
|
||||
0 AS qty_out,
|
||||
COALESCE(p.product_price, 0) AS price
|
||||
`).
|
||||
Joins("JOIN product_warehouses AS pw ON pw.id = pc.product_warehouse_id").
|
||||
Joins("JOIN products AS p ON p.id = pw.product_id").
|
||||
Joins("LEFT JOIN purchase_items AS pi ON pi.product_warehouse_id = pc.product_warehouse_id").
|
||||
Joins("LEFT JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Where("pc.project_flock_kandang_id IN ?", pfkIDs).
|
||||
Where("pc.usage_qty > 0").
|
||||
Group("pw.product_id, p.name, f.name")
|
||||
|
||||
var chickinRows []ActualUsageCostRow
|
||||
if err := chickinQuery.Scan(&chickinRows).Error; err != nil {
|
||||
return nil, err
|
||||
Joins("JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
|
||||
Joins("LEFT JOIN laying_transfer_sources lts ON lts.laying_transfer_id = lt.id").
|
||||
Joins("LEFT JOIN product_warehouses pw_source ON pw_source.id = lts.product_warehouse_id").
|
||||
Joins("LEFT JOIN warehouses w_source ON w_source.id = pw_source.warehouse_id").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = ltt.product_warehouse_id").
|
||||
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Where("w.kandang_id = ?", kandangID).
|
||||
Where("(w_source.kandang_id IS NULL OR w_source.kandang_id <> w.kandang_id)").
|
||||
Where("f.name IN ?", sapronakFlagsAll)
|
||||
incomingLaying, err := scanAndGroupDetails(incomingLayingQuery)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for pid, rows := range incomingLaying {
|
||||
incoming[pid] = append(incoming[pid], rows...)
|
||||
}
|
||||
|
||||
rows = append(rows, chickinRows...)
|
||||
outgoingQuery := r.withCtx(ctx).
|
||||
Table("stock_allocations AS sa").
|
||||
Select(`
|
||||
std.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
st.transfer_date::timestamp AS date,
|
||||
COALESCE(st.movement_number, '') AS reference,
|
||||
0 AS qty_in,
|
||||
COALESCE(SUM(sa.qty), 0) AS qty_out,
|
||||
COALESCE(p.product_price, 0) AS price
|
||||
`).
|
||||
Joins("JOIN stock_transfer_details std ON std.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyStockTransferOut.String()).
|
||||
Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
|
||||
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
||||
Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = std.dest_product_warehouse_id").
|
||||
Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id").
|
||||
Joins("JOIN products p ON p.id = std.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Where("sa.status = ?", entity.StockAllocationStatusActive).
|
||||
Where("w.kandang_id = ?", kandangID).
|
||||
Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)").
|
||||
Where("f.name IN ?", sapronakFlagsAll).
|
||||
Group("std.id, std.product_id, p.name, f.name, st.transfer_date, st.movement_number, p.product_price")
|
||||
outgoing, err := scanAndGroupDetails(outgoingQuery)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
outgoingLayingQuery := r.withCtx(ctx).
|
||||
Table("stock_allocations AS sa").
|
||||
Select(`
|
||||
pw.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
lt.transfer_date::timestamp AS date,
|
||||
COALESCE(lt.transfer_number, '') AS reference,
|
||||
0 AS qty_in,
|
||||
COALESCE(SUM(sa.qty), 0) AS qty_out,
|
||||
COALESCE(p.product_price, 0) AS price
|
||||
`).
|
||||
Joins("JOIN laying_transfer_sources lts ON lts.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyTransferToLayingOut.String()).
|
||||
Joins("JOIN laying_transfers lt ON lt.id = lts.laying_transfer_id").
|
||||
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.laying_transfer_id = lt.id").
|
||||
Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = ltt.product_warehouse_id").
|
||||
Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
|
||||
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Where("sa.status = ?", entity.StockAllocationStatusActive).
|
||||
Where("w.kandang_id = ?", kandangID).
|
||||
Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)").
|
||||
Where("f.name IN ?", sapronakFlagsAll).
|
||||
Group("lts.id, pw.product_id, p.name, f.name, lt.transfer_date, lt.transfer_number, p.product_price")
|
||||
outgoingLaying, err := scanAndGroupDetails(outgoingLayingQuery)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for pid, rows := range outgoingLaying {
|
||||
outgoing[pid] = append(outgoing[pid], rows...)
|
||||
}
|
||||
|
||||
return incoming, outgoing, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) {
|
||||
query := r.withCtx(ctx).
|
||||
Table("stock_allocations AS sa").
|
||||
Select(`
|
||||
pw.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
COALESCE(mdp.delivery_date, mdp.created_at) AS date,
|
||||
COALESCE(m.so_number, '') AS reference,
|
||||
0 AS qty_in,
|
||||
COALESCE(SUM(sa.qty), 0) AS qty_out,
|
||||
COALESCE(mdp.unit_price, mp.unit_price, 0) AS price
|
||||
`).
|
||||
Joins("JOIN marketing_delivery_products mdp ON mdp.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyMarketingDelivery.String()).
|
||||
Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
|
||||
Joins("JOIN marketings m ON m.id = mp.marketing_id").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
|
||||
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Where("sa.status = ?", entity.StockAllocationStatusActive).
|
||||
Where("w.kandang_id = ?", kandangID).
|
||||
Where("f.name IN ?", sapronakFlagsAll).
|
||||
Group("mdp.id, pw.product_id, p.name, f.name, mdp.delivery_date, mdp.created_at, m.so_number, mdp.unit_price, mp.unit_price")
|
||||
|
||||
return scanAndGroupDetails(query)
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ClosingKeuanganRepository handles database operations for closing keuangan
|
||||
type ClosingKeuanganRepository interface {
|
||||
repository.BaseRepository[interface{}]
|
||||
|
||||
// All Product Usage
|
||||
GetAllProductUsageByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, flagFilters []string) ([]ProductUsageRow, error)
|
||||
|
||||
// Depletion per kandang
|
||||
GetTotalDepletionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||
|
||||
// Weight produced from uniformity per kandang
|
||||
GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||
|
||||
// DB returns the underlying GORM DB instance
|
||||
DB() *gorm.DB
|
||||
}
|
||||
|
||||
type ClosingKeuanganRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[interface{}]
|
||||
}
|
||||
|
||||
func NewClosingKeuanganRepository(db *gorm.DB) ClosingKeuanganRepository {
|
||||
return &ClosingKeuanganRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[interface{}](db),
|
||||
}
|
||||
}
|
||||
|
||||
// Result Rows
|
||||
|
||||
type ProductUsageRow struct {
|
||||
ProductID uint `gorm:"column:product_id"`
|
||||
ProductName string `gorm:"column:product_name"`
|
||||
FlagNames string `gorm:"column:flag_names"`
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
Price float64 `gorm:"column:price"`
|
||||
TotalPengeluaran float64 `gorm:"column:total_pengeluaran"`
|
||||
}
|
||||
|
||||
// GetAllProductUsageByProjectFlockKandangID gets all product usage for a project flock kandang
|
||||
// Combines data from all usable types: recordings, chickins, marketing, transfers, adjustments
|
||||
// flagFilters: optional filter to get only specific flags (e.g., ["PAKAN", "OVK"]), empty means get all
|
||||
func (r *ClosingKeuanganRepositoryImpl) GetAllProductUsageByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, flagFilters []string) ([]ProductUsageRow, error) {
|
||||
if projectFlockKandangID == 0 {
|
||||
return []ProductUsageRow{}, nil
|
||||
}
|
||||
|
||||
type SubQueryResult struct {
|
||||
ProductID uint `gorm:"column:product_id"`
|
||||
ProductName string `gorm:"column:product_name"`
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
Price float64 `gorm:"column:price"`
|
||||
}
|
||||
|
||||
type AggregatedResult struct {
|
||||
ProductID uint `gorm:"column:product_id"`
|
||||
ProductName string `gorm:"column:product_name"`
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
Price float64 `gorm:"column:price"`
|
||||
PriceCount int `gorm:"-"` // For calculating average price
|
||||
}
|
||||
|
||||
type FlagResult struct {
|
||||
ProductID uint `gorm:"column:product_id"`
|
||||
FlagNames string `gorm:"column:flag_names"`
|
||||
}
|
||||
|
||||
var allResults []SubQueryResult
|
||||
|
||||
// Subquery 1: Recordings
|
||||
var recordingsResults []SubQueryResult
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("recordings r").
|
||||
Select("pw.product_id, p.name as product_name, "+
|
||||
"COALESCE(SUM(CASE "+
|
||||
"WHEN sa.stockable_type = 'PURCHASE_ITEMS' THEN COALESCE(sa.qty, 0) "+
|
||||
"WHEN sa.stockable_type = 'STOCK_TRANSFER_IN' THEN COALESCE(std.usage_qty, 0) "+
|
||||
"WHEN sa.stockable_type = 'TRANSFERTOLAYING_IN' THEN COALESCE(ltt.total_used, 0) "+
|
||||
"WHEN sa.stockable_type = 'ADJUSTMENT_IN' THEN COALESCE(adjs.total_used, 0) "+
|
||||
"WHEN sa.stockable_type = 'PROJECT_FLOCK_POPULATION' THEN COALESCE(pfp.total_used_qty, 0) "+
|
||||
"ELSE 0 END), 0) as total_qty, "+
|
||||
"COALESCE(AVG(CASE WHEN sa.stockable_type = 'PURCHASE_ITEMS' THEN COALESCE(pi.price, 0) END), 0) as price").
|
||||
Joins("JOIN recording_stocks rs ON rs.recording_id = r.id").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Joins("LEFT JOIN stock_allocations sa ON sa.usable_type = 'RECORDING_STOCK' AND sa.usable_id = rs.id AND sa.status = 'ACTIVE'").
|
||||
Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = 'PURCHASE_ITEMS'").
|
||||
Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = 'STOCK_TRANSFER_IN'").
|
||||
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = 'TRANSFERTOLAYING_IN'").
|
||||
Joins("LEFT JOIN adjustment_stocks adjs ON adjs.id = sa.stockable_id AND sa.stockable_type = 'ADJUSTMENT_IN'").
|
||||
Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = 'PROJECT_FLOCK_POPULATION'").
|
||||
Where("r.project_flock_kandangs_id = ?", projectFlockKandangID).
|
||||
Where("r.deleted_at IS NULL").
|
||||
Group("pw.product_id, p.name").
|
||||
Scan(&recordingsResults).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get recordings product usage: %w", err)
|
||||
}
|
||||
fmt.Printf("[REPO] Recordings query: %d results for projectFlockKandangID=%d\n", len(recordingsResults), projectFlockKandangID)
|
||||
allResults = append(allResults, recordingsResults...)
|
||||
|
||||
// Subquery 2: Chickins
|
||||
var chickinsResults []SubQueryResult
|
||||
err = r.DB().WithContext(ctx).
|
||||
Table("project_chickins pc").
|
||||
Select("pw.product_id, p.name as product_name, "+
|
||||
"COALESCE(SUM(pc.usage_qty), 0) as total_qty, "+
|
||||
"COALESCE(AVG(COALESCE(pi.price, 0)), 0) as price").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = pc.product_warehouse_id").
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id").
|
||||
Where("pc.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Where("pc.usage_qty > 0").
|
||||
Group("pw.product_id, p.name").
|
||||
Scan(&chickinsResults).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get chickins product usage: %w", err)
|
||||
}
|
||||
fmt.Printf("[REPO] Chickins query: %d results for projectFlockKandangID=%d\n", len(chickinsResults), projectFlockKandangID)
|
||||
allResults = append(allResults, chickinsResults...)
|
||||
|
||||
// Subquery 3: Marketing Delivery
|
||||
var marketingResults []SubQueryResult
|
||||
err = r.DB().WithContext(ctx).
|
||||
Table("marketing_delivery_products mdp").
|
||||
Select("pw.product_id, p.name as product_name, "+
|
||||
"COALESCE(SUM(mdp.usage_qty), 0) as total_qty, "+
|
||||
"COALESCE(AVG(COALESCE(pi.price, 0)), 0) as price").
|
||||
Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id").
|
||||
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Group("pw.product_id, p.name").
|
||||
Scan(&marketingResults).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get marketing product usage: %w", err)
|
||||
}
|
||||
fmt.Printf("[REPO] Marketing query: %d results for projectFlockKandangID=%d\n", len(marketingResults), projectFlockKandangID)
|
||||
allResults = append(allResults, marketingResults...)
|
||||
|
||||
// Subquery 4: Laying Transfer Sources
|
||||
var layingTransferResults []SubQueryResult
|
||||
err = r.DB().WithContext(ctx).
|
||||
Table("laying_transfer_sources lts").
|
||||
Select("pw.product_id, p.name as product_name, "+
|
||||
"COALESCE(SUM(lts.usage_qty), 0) as total_qty, "+
|
||||
"COALESCE(AVG(COALESCE(pi.price, 0)), 0) as price").
|
||||
Joins("JOIN laying_transfers lt ON lt.id = lts.laying_transfer_id").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = lts.product_warehouse_id").
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id").
|
||||
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Group("pw.product_id, p.name").
|
||||
Scan(&layingTransferResults).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get laying transfer product usage: %w", err)
|
||||
}
|
||||
fmt.Printf("[REPO] Laying Transfer query: %d results for projectFlockKandangID=%d\n", len(layingTransferResults), projectFlockKandangID)
|
||||
allResults = append(allResults, layingTransferResults...)
|
||||
|
||||
// Subquery 5: Stock Transfer Details
|
||||
var stockTransferResults []SubQueryResult
|
||||
err = r.DB().WithContext(ctx).
|
||||
Table("stock_transfer_details std").
|
||||
Select("pw.product_id, p.name as product_name, "+
|
||||
"COALESCE(SUM(std.usage_qty), 0) as total_qty, "+
|
||||
"COALESCE(AVG(COALESCE(pi.price, 0)), 0) as price").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = std.source_product_warehouse_id").
|
||||
Joins("JOIN products p ON p.id = std.product_id").
|
||||
Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id").
|
||||
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Group("pw.product_id, p.name").
|
||||
Scan(&stockTransferResults).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get stock transfer product usage: %w", err)
|
||||
}
|
||||
fmt.Printf("[REPO] Stock Transfer query: %d results for projectFlockKandangID=%d\n", len(stockTransferResults), projectFlockKandangID)
|
||||
allResults = append(allResults, stockTransferResults...)
|
||||
|
||||
// Subquery 6: Adjustment Stocks
|
||||
var adjustmentResults []SubQueryResult
|
||||
err = r.DB().WithContext(ctx).
|
||||
Table("adjustment_stocks ads").
|
||||
Select("pw.product_id, p.name as product_name, "+
|
||||
"COALESCE(SUM(ads.usage_qty), 0) as total_qty, "+
|
||||
"COALESCE(AVG(COALESCE(pi.price, 0)), 0) as price").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = ads.product_warehouse_id").
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id").
|
||||
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Where("ads.usage_qty > 0").
|
||||
Group("pw.product_id, p.name").
|
||||
Scan(&adjustmentResults).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get adjustment product usage: %w", err)
|
||||
}
|
||||
fmt.Printf("[REPO] Adjustment query: %d results for projectFlockKandangID=%d\n", len(adjustmentResults), projectFlockKandangID)
|
||||
allResults = append(allResults, adjustmentResults...)
|
||||
|
||||
fmt.Printf("[REPO] Total raw results before aggregation: %d items\n", len(allResults))
|
||||
|
||||
// Aggregate results by product_id
|
||||
aggregatedMap := make(map[uint]*AggregatedResult)
|
||||
for _, result := range allResults {
|
||||
key := result.ProductID
|
||||
if existing, exists := aggregatedMap[key]; exists {
|
||||
existing.TotalQty += result.TotalQty
|
||||
existing.Price += result.Price
|
||||
existing.PriceCount++
|
||||
} else {
|
||||
aggregatedMap[key] = &AggregatedResult{
|
||||
ProductID: result.ProductID,
|
||||
ProductName: result.ProductName,
|
||||
TotalQty: result.TotalQty,
|
||||
Price: result.Price,
|
||||
PriceCount: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[REPO] Aggregated to %d unique products\n", len(aggregatedMap))
|
||||
|
||||
// Get flags for all products
|
||||
productIDs := make([]uint, 0, len(aggregatedMap))
|
||||
for id := range aggregatedMap {
|
||||
productIDs = append(productIDs, id)
|
||||
}
|
||||
|
||||
var flagResults []FlagResult
|
||||
if len(productIDs) > 0 {
|
||||
err = r.DB().WithContext(ctx).
|
||||
Table("products p").
|
||||
Select("p.id as product_id, STRING_AGG(DISTINCT f.name, ', ') as flag_names").
|
||||
Joins("LEFT JOIN flags f ON f.flagable_type = 'products' AND f.flagable_id = p.id").
|
||||
Where("p.id IN ?", productIDs).
|
||||
Group("p.id").
|
||||
Scan(&flagResults).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get product flags: %w", err)
|
||||
}
|
||||
}
|
||||
fmt.Printf("[REPO] Fetched flags for %d products\n", len(flagResults))
|
||||
|
||||
// Build flag map
|
||||
flagMap := make(map[uint]string)
|
||||
for _, flag := range flagResults {
|
||||
flagMap[flag.ProductID] = flag.FlagNames
|
||||
}
|
||||
|
||||
// Combine results and calculate average price
|
||||
results := make([]ProductUsageRow, 0, len(aggregatedMap))
|
||||
for _, agg := range aggregatedMap {
|
||||
avgPrice := float64(0)
|
||||
if agg.PriceCount > 0 {
|
||||
avgPrice = agg.Price / float64(agg.PriceCount)
|
||||
}
|
||||
|
||||
flagNames := flagMap[agg.ProductID]
|
||||
|
||||
// Apply flag filters if provided
|
||||
if len(flagFilters) > 0 {
|
||||
// Check if any of the flagFilters exist in flagNames
|
||||
matched := false
|
||||
for _, filter := range flagFilters {
|
||||
if containsIgnoreCase(flagNames, filter) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
continue // Skip this product if no flag matches
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, ProductUsageRow{
|
||||
ProductID: agg.ProductID,
|
||||
ProductName: agg.ProductName,
|
||||
FlagNames: flagNames,
|
||||
TotalQty: agg.TotalQty,
|
||||
Price: avgPrice,
|
||||
TotalPengeluaran: agg.TotalQty * avgPrice,
|
||||
})
|
||||
}
|
||||
|
||||
fmt.Printf("[REPO] After filtering with flagFilters=%v: %d results\n", flagFilters, len(results))
|
||||
for i, r := range results {
|
||||
fmt.Printf("[REPO] Result[%d]: ProductID=%d, ProductName=%s, FlagNames=%s, TotalQty=%.2f, Price=%.2f, TotalPengeluaran=%.2f\n",
|
||||
i, r.ProductID, r.ProductName, r.FlagNames, r.TotalQty, r.Price, r.TotalPengeluaran)
|
||||
}
|
||||
|
||||
// Sort by product name
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].ProductName < results[j].ProductName
|
||||
})
|
||||
|
||||
fmt.Printf("[REPO] Final sorted results: %d items\n", len(results))
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetTotalDepletionByProjectFlockKandangID gets total depletion for a specific kandang
|
||||
func (r *ClosingKeuanganRepositoryImpl) GetTotalDepletionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
|
||||
var result float64
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("recording_depletions").
|
||||
Select("COALESCE(SUM(recording_depletions.qty), 0)").
|
||||
Joins("JOIN recordings ON recordings.id = recording_depletions.recording_id").
|
||||
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id").
|
||||
Where("project_flock_kandangs.id = ?", projectFlockKandangID).
|
||||
Scan(&result).Error
|
||||
return result, err
|
||||
}
|
||||
|
||||
// GetTotalWeightProducedFromUniformityByProjectFlockKandangID calculates total weight produced from uniformity data for a specific kandang
|
||||
// Formula: (mean_up / 1.10) * chick_qty_of_weight / 1000
|
||||
func (r *ClosingKeuanganRepositoryImpl) GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
|
||||
if projectFlockKandangID == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var uniformity struct {
|
||||
MeanUp float64
|
||||
ChickQtyOfWeight float64
|
||||
}
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("project_flock_kandang_uniformity").
|
||||
Select("mean_up, chick_qty_of_weight").
|
||||
Where("project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Order("id DESC").
|
||||
Limit(1).
|
||||
Scan(&uniformity).Error
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Calculate weight: (mean_up / 1.10) * chick_qty_of_weight / 1000
|
||||
totalWeight := (uniformity.MeanUp / 1.10) * uniformity.ChickQtyOfWeight / 1000
|
||||
|
||||
return totalWeight, nil
|
||||
}
|
||||
|
||||
// containsIgnoreCase checks if a string contains a substring (case-insensitive)
|
||||
func containsIgnoreCase(str, substr string) bool {
|
||||
return strings.Contains(strings.ToUpper(str), strings.ToUpper(substr))
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService) {
|
||||
ctrl := controller.NewClosingController(s, sapronakSvc)
|
||||
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService, closingKeuanganSvc closing.ClosingKeuanganService) {
|
||||
ctrl := controller.NewClosingController(s, sapronakSvc, closingKeuanganSvc)
|
||||
|
||||
route := v1.Group("/closings")
|
||||
route.Use(m.Auth(u))
|
||||
@@ -23,13 +23,18 @@ 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)
|
||||
route.Get("/:projectFlockId/sapronak/summary", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronakSummary)
|
||||
route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPP)
|
||||
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)
|
||||
route.Get("/:project_flock_id/:project_flock_kandang_id/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuanganByKandang)
|
||||
|
||||
}
|
||||
|
||||
@@ -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,12 +35,12 @@ 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)
|
||||
GetClosingSapronakSummary(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakSummaryItemDTO, 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("flock_name LIKE ?", "%"+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")
|
||||
@@ -230,7 +337,9 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
|
||||
}
|
||||
|
||||
var projectFlockKandangIDs []uint
|
||||
if params.Type == validation.SapronakTypeOutgoing {
|
||||
if params.KandangID != nil && *params.KandangID > 0 {
|
||||
projectFlockKandangIDs = []uint{*params.KandangID}
|
||||
} else if params.Type == validation.SapronakTypeOutgoing {
|
||||
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
|
||||
@@ -245,6 +354,7 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
|
||||
ProjectFlockKandangIDs: projectFlockKandangIDs,
|
||||
Limit: params.Limit,
|
||||
Offset: offset,
|
||||
Search: params.Search,
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err)
|
||||
@@ -279,6 +389,74 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
|
||||
return items, totalResults, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetClosingSapronakSummary(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakSummaryItemDTO, error) {
|
||||
if projectFlockID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||
}
|
||||
|
||||
if params == nil {
|
||||
params = &validation.ClosingSapronakQuery{}
|
||||
}
|
||||
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
if params.Type != validation.SapronakTypeIncoming && params.Type != validation.SapronakTypeOutgoing {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
|
||||
}
|
||||
|
||||
if _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
|
||||
}
|
||||
s.Log.Errorf("Failed get project flock %d for sapronak closing summary: %+v", projectFlockID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||
}
|
||||
|
||||
warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouses for project flock")
|
||||
}
|
||||
|
||||
var projectFlockKandangIDs []uint
|
||||
if params.KandangID != nil && *params.KandangID > 0 {
|
||||
projectFlockKandangIDs = []uint{*params.KandangID}
|
||||
} else if params.Type == validation.SapronakTypeOutgoing {
|
||||
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := s.Repository.GetSapronakSummary(c.Context(), repository.SapronakQueryParams{
|
||||
Type: params.Type,
|
||||
WarehouseIDs: warehouseIDs,
|
||||
ProjectFlockKandangIDs: projectFlockKandangIDs,
|
||||
Search: params.Search,
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch sapronak %s summary for project flock %d: %+v", params.Type, projectFlockID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sapronak summary data")
|
||||
}
|
||||
|
||||
items := make([]dto.ClosingSapronakSummaryItemDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, dto.ClosingSapronakSummaryItemDTO{
|
||||
Category: row.Category,
|
||||
TotalQty: row.TotalQty,
|
||||
Uom: dto.UomSummaryDTO{
|
||||
ID: row.UomID,
|
||||
Name: row.UomName,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) {
|
||||
var kandangIDs []uint
|
||||
db := s.Repository.DB().WithContext(ctx)
|
||||
@@ -313,10 +491,10 @@ func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, proje
|
||||
|
||||
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID 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)
|
||||
err := query.Order("id ASC").Pluck("id", &ids).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -379,115 +557,94 @@ 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
|
||||
}
|
||||
|
||||
func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) {
|
||||
|
||||
if err := commonSvc.EnsureRelations(c.Context(),
|
||||
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||
}
|
||||
|
||||
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
|
||||
}
|
||||
|
||||
actualUsageRows, err := s.Repository.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost")
|
||||
}
|
||||
|
||||
purchaseItems := s.convertActualUsageToPurchaseItems(c.Context(), actualUsageRows)
|
||||
|
||||
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations")
|
||||
}
|
||||
|
||||
deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("MarketingProduct").
|
||||
Preload("MarketingProduct.ProductWarehouse").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product")
|
||||
})
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products")
|
||||
}
|
||||
|
||||
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins")
|
||||
}
|
||||
|
||||
totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err)
|
||||
}
|
||||
|
||||
totalEggWeightKg, err := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Warnf("GetTotalEggProductionWeightByProjectFlockID error: %v", err)
|
||||
}
|
||||
|
||||
totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
|
||||
}
|
||||
|
||||
input := dto.ClosingKeuanganInput{
|
||||
ProjectFlockCategory: projectFlock.Category,
|
||||
PurchaseItems: purchaseItems,
|
||||
Budgets: budgets,
|
||||
Realizations: realizations,
|
||||
DeliveryProducts: deliveryProducts,
|
||||
Chickins: chickins,
|
||||
TotalWeightProduced: totalWeightProduced,
|
||||
TotalEggWeightKg: totalEggWeightKg,
|
||||
TotalDepletion: totalDepletion,
|
||||
}
|
||||
|
||||
report := dto.ToClosingKeuanganReport(input)
|
||||
|
||||
return &report, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) {
|
||||
if projectFlockID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||
@@ -520,12 +677,28 @@ 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)
|
||||
var projectFlockKandangIDs []uint
|
||||
if kandangID != nil && *kandangID > 0 {
|
||||
projectFlockKandangIDs = []uint{*kandangID}
|
||||
} else {
|
||||
var err error
|
||||
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
|
||||
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 +707,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 +738,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 +794,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 +805,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 +838,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 +888,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 +907,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 +1009,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 +1079,7 @@ func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopul
|
||||
FcrStd: fcrStd,
|
||||
FcrAct: fcrAct,
|
||||
DeffFcr: deffFcr,
|
||||
Awg: awg,
|
||||
AwgAct: awg,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -789,53 +1100,3 @@ func closestFcrValues(standards []entity.FcrStandard, averageWeight float64) (fl
|
||||
|
||||
return closest.Mortality, closest.FcrNumber
|
||||
}
|
||||
|
||||
func (s closingService) convertActualUsageToPurchaseItems(ctx context.Context, actualUsageRows []repository.ActualUsageCostRow) []entity.PurchaseItem {
|
||||
if len(actualUsageRows) == 0 {
|
||||
return []entity.PurchaseItem{}
|
||||
}
|
||||
|
||||
// Collect all product IDs
|
||||
productIDs := make([]uint, len(actualUsageRows))
|
||||
for i, row := range actualUsageRows {
|
||||
productIDs[i] = row.ProductID
|
||||
}
|
||||
|
||||
// Fetch products with flags from repository
|
||||
products, err := s.Repository.GetProductsWithFlagsByIDs(ctx, productIDs)
|
||||
if err != nil {
|
||||
s.Log.Warnf("Failed to fetch products for actual usage: %v", err)
|
||||
products = []entity.Product{}
|
||||
}
|
||||
|
||||
// Create product map
|
||||
productMap := make(map[uint]*entity.Product)
|
||||
for i := range products {
|
||||
productMap[products[i].Id] = &products[i]
|
||||
}
|
||||
|
||||
// Convert to pseudo purchase items
|
||||
purchaseItems := make([]entity.PurchaseItem, 0, len(actualUsageRows))
|
||||
for _, row := range actualUsageRows {
|
||||
product := productMap[row.ProductID]
|
||||
|
||||
// Skip if product not found
|
||||
if product == nil {
|
||||
s.Log.Warnf("Product ID %d not found for actual usage", row.ProductID)
|
||||
continue
|
||||
}
|
||||
|
||||
purchaseItem := entity.PurchaseItem{
|
||||
Id: 0, // Pseudo item, no ID
|
||||
ProductId: row.ProductID,
|
||||
TotalQty: row.TotalQty,
|
||||
TotalPrice: row.TotalPrice,
|
||||
Price: row.AveragePrice,
|
||||
Product: product,
|
||||
}
|
||||
|
||||
purchaseItems = append(purchaseItems, purchaseItem)
|
||||
}
|
||||
|
||||
return purchaseItems
|
||||
}
|
||||
|
||||
@@ -0,0 +1,640 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/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"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ClosingKeuanganService handles closing keuangan business logic
|
||||
type ClosingKeuanganService interface {
|
||||
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingKeuanganData, error)
|
||||
GetClosingKeuanganByKandang(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error)
|
||||
}
|
||||
|
||||
type closingKeuanganService struct {
|
||||
Log *logrus.Logger
|
||||
ClosingKeuanganRepo repository.ClosingKeuanganRepository
|
||||
ProjectFlockRepo projectflockRepository.ProjectflockRepository
|
||||
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
|
||||
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
|
||||
ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository
|
||||
ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository
|
||||
ChickinRepo chickinRepository.ProjectChickinRepository
|
||||
RecordingRepo recordingRepository.RecordingRepository
|
||||
}
|
||||
|
||||
func NewClosingKeuanganService(
|
||||
closingKeuanganRepo repository.ClosingKeuanganRepository,
|
||||
projectFlockRepo projectflockRepository.ProjectflockRepository,
|
||||
projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository,
|
||||
marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository,
|
||||
expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository,
|
||||
projectBudgetRepo projectflockRepository.ProjectBudgetRepository,
|
||||
chickinRepo chickinRepository.ProjectChickinRepository,
|
||||
recordingRepo recordingRepository.RecordingRepository,
|
||||
) ClosingKeuanganService {
|
||||
return &closingKeuanganService{
|
||||
Log: utils.Log,
|
||||
ClosingKeuanganRepo: closingKeuanganRepo,
|
||||
ProjectFlockRepo: projectFlockRepo,
|
||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
||||
ExpenseRealizationRepo: expenseRealizationRepo,
|
||||
ProjectBudgetRepo: projectBudgetRepo,
|
||||
ChickinRepo: chickinRepo,
|
||||
RecordingRepo: recordingRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingKeuanganData, error) {
|
||||
|
||||
if err := commonSvc.EnsureRelations(c.Context(),
|
||||
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||
}
|
||||
|
||||
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
|
||||
}
|
||||
|
||||
// Preload Nonstock.Flags manually
|
||||
var budgetIDs []uint
|
||||
for _, b := range budgets {
|
||||
budgetIDs = append(budgetIDs, b.Id)
|
||||
}
|
||||
if len(budgetIDs) > 0 {
|
||||
err = s.ProjectBudgetRepo.DB().WithContext(c.Context()).
|
||||
Preload("Nonstock.Flags").
|
||||
Where("id IN ?", budgetIDs).
|
||||
Find(&budgets).Error
|
||||
}
|
||||
|
||||
// Get all kandang for this project flock
|
||||
kandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs")
|
||||
}
|
||||
|
||||
return s.calculateClosingKeuangan(c, projectFlock, budgets, kandangs, projectFlockID)
|
||||
}
|
||||
|
||||
func (s closingKeuanganService) GetClosingKeuanganByKandang(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error) {
|
||||
|
||||
if err := commonSvc.EnsureRelations(c.Context(),
|
||||
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validate and fetch project flock kandang
|
||||
kandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), projectFlockKandangID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found")
|
||||
}
|
||||
if kandang.ProjectFlockId != projectFlockID {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang does not belong to this project flock")
|
||||
}
|
||||
|
||||
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||
}
|
||||
|
||||
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
|
||||
}
|
||||
|
||||
// Preload Nonstock.Flags manually
|
||||
var budgetIDs []uint
|
||||
for _, b := range budgets {
|
||||
budgetIDs = append(budgetIDs, b.Id)
|
||||
}
|
||||
if len(budgetIDs) > 0 {
|
||||
err = s.ProjectBudgetRepo.DB().WithContext(c.Context()).
|
||||
Preload("Nonstock.Flags").
|
||||
Where("id IN ?", budgetIDs).
|
||||
Find(&budgets).Error
|
||||
}
|
||||
|
||||
kandangs := []entity.ProjectFlockKandang{*kandang}
|
||||
|
||||
return s.calculateClosingKeuangan(c, projectFlock, budgets, kandangs, projectFlockID)
|
||||
}
|
||||
|
||||
func (s closingKeuanganService) calculateClosingKeuangan(c *fiber.Ctx, projectFlock *entity.ProjectFlock, budgets []entity.ProjectBudget, kandangs []entity.ProjectFlockKandang, scopeID uint) (*dto.ClosingKeuanganData, error) {
|
||||
// Define flag filters using constants
|
||||
pakanFilters := []string{string(utils.FlagPakan), string(utils.FlagPreStarter), string(utils.FlagStarter), string(utils.FlagFinisher)}
|
||||
ovkFilters := []string{string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia)}
|
||||
ayamFilters := []string{string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer)}
|
||||
allFilters := append(pakanFilters, ovkFilters...)
|
||||
allFilters = append(allFilters, ayamFilters...)
|
||||
|
||||
var allProductUsageRows []repository.ProductUsageRow
|
||||
|
||||
// Get ALL product usage
|
||||
for _, kandang := range kandangs {
|
||||
rows, err := s.ClosingKeuanganRepo.GetAllProductUsageByProjectFlockKandangID(c.Context(), kandang.Id, allFilters)
|
||||
if err == nil {
|
||||
allProductUsageRows = append(allProductUsageRows, rows...)
|
||||
}
|
||||
}
|
||||
|
||||
// Classify into categories based on flag priority
|
||||
var pakanProductUsageRows []repository.ProductUsageRow
|
||||
var ovkProductUsageRows []repository.ProductUsageRow
|
||||
var ayamProductUsageRows []repository.ProductUsageRow
|
||||
|
||||
for _, row := range allProductUsageRows {
|
||||
// Parse flag names from comma-separated string
|
||||
flagNames := strings.Split(row.FlagNames, ",")
|
||||
|
||||
hasPakanFlag := false
|
||||
hasOvkFlag := false
|
||||
hasAyamFlag := false
|
||||
|
||||
for _, flag := range flagNames {
|
||||
flag = strings.TrimSpace(flag)
|
||||
if containsItem(pakanFilters, flag) {
|
||||
hasPakanFlag = true
|
||||
}
|
||||
if containsItem(ovkFilters, flag) {
|
||||
hasOvkFlag = true
|
||||
}
|
||||
if containsItem(ayamFilters, flag) {
|
||||
hasAyamFlag = true
|
||||
}
|
||||
}
|
||||
|
||||
// Priority: PAKAN > OVK > AYAM
|
||||
if hasPakanFlag {
|
||||
pakanProductUsageRows = append(pakanProductUsageRows, row)
|
||||
} else if hasOvkFlag {
|
||||
ovkProductUsageRows = append(ovkProductUsageRows, row)
|
||||
} else if hasAyamFlag {
|
||||
ayamProductUsageRows = append(ayamProductUsageRows, row)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Calculate total price for each category
|
||||
var totalPakanPrice, totalOvkPrice, totalAyamPrice float64
|
||||
for _, row := range pakanProductUsageRows {
|
||||
totalPakanPrice += row.TotalPengeluaran
|
||||
}
|
||||
for _, row := range ovkProductUsageRows {
|
||||
totalOvkPrice += row.TotalPengeluaran
|
||||
}
|
||||
for _, row := range ayamProductUsageRows {
|
||||
totalAyamPrice += row.TotalPengeluaran
|
||||
}
|
||||
|
||||
// Determine if this is per-kandang or per-project-flock scope
|
||||
isPerKandang := len(kandangs) == 1
|
||||
var projectFlockKandangID *uint
|
||||
if isPerKandang {
|
||||
kandangID := kandangs[0].Id
|
||||
projectFlockKandangID = &kandangID
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
// Fetch realizations
|
||||
var realizations []entity.ExpenseRealization
|
||||
if isPerKandang && projectFlockKandangID != nil {
|
||||
realizations, err = s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, projectFlockKandangID)
|
||||
} else {
|
||||
realizations, err = s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, nil)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations")
|
||||
}
|
||||
|
||||
deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlock.Id, func(db *gorm.DB) *gorm.DB {
|
||||
db = db.Preload("MarketingProduct").
|
||||
Preload("MarketingProduct.ProductWarehouse").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product")
|
||||
return db
|
||||
})
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products")
|
||||
}
|
||||
|
||||
// Filter by kandang if scope is per-kandang (manual filtering after fetch)
|
||||
if isPerKandang && projectFlockKandangID != nil {
|
||||
filteredProducts := make([]entity.MarketingDeliveryProduct, 0)
|
||||
for _, dp := range deliveryProducts {
|
||||
pfKandangID := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandangId
|
||||
if pfKandangID != nil && *pfKandangID == *projectFlockKandangID {
|
||||
filteredProducts = append(filteredProducts, dp)
|
||||
}
|
||||
}
|
||||
deliveryProducts = filteredProducts
|
||||
}
|
||||
|
||||
// Fetch chickins
|
||||
var chickins []entity.ProjectChickin
|
||||
if isPerKandang && projectFlockKandangID != nil {
|
||||
chickins, err = s.ChickinRepo.GetByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
|
||||
} else {
|
||||
chickins, err = s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlock.Id)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins")
|
||||
}
|
||||
|
||||
// Get total depletion
|
||||
var totalDepletion float64
|
||||
if isPerKandang && projectFlockKandangID != nil {
|
||||
totalDepletion, err = s.ClosingKeuanganRepo.GetTotalDepletionByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
|
||||
} else {
|
||||
totalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlock.Id)
|
||||
}
|
||||
if err != nil {
|
||||
totalDepletion = 0
|
||||
}
|
||||
|
||||
totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlock.Id)
|
||||
if err != nil {
|
||||
}
|
||||
|
||||
// Try to get actual weight from uniformity data
|
||||
var totalWeightFromUniformity float64
|
||||
if isPerKandang && projectFlockKandangID != nil {
|
||||
totalWeightFromUniformity, err = s.ClosingKeuanganRepo.GetTotalWeightProducedFromUniformityByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
|
||||
} else {
|
||||
totalWeightFromUniformity, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockID(c.Context(), projectFlock.Id)
|
||||
}
|
||||
if err != nil {
|
||||
} else if totalWeightFromUniformity > 0 {
|
||||
totalWeightProduced = totalWeightFromUniformity
|
||||
}
|
||||
|
||||
// Fetch egg data only for Laying category
|
||||
var totalEggWeightKg float64
|
||||
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
||||
// TODO: Replace with actual method to get egg weight from RecordingRepo
|
||||
// totalEggWeightKg, err = s.RecordingRepo.GetEggWeightByProjectFlockID(c.Context(), projectFlock.Id)
|
||||
// For now, set to 0 as placeholder
|
||||
totalEggWeightKg = 0
|
||||
} else {
|
||||
totalEggWeightKg = 0
|
||||
}
|
||||
|
||||
// Build new DTO structure
|
||||
|
||||
// Calculate totals
|
||||
var totalPopulation float64
|
||||
for _, chickin := range chickins {
|
||||
totalPopulation += chickin.UsageQty
|
||||
}
|
||||
|
||||
// Calculate actual population (total population - depletion)
|
||||
actualPopulation := totalPopulation - totalDepletion
|
||||
|
||||
// Calculate budget totals by category
|
||||
calculateBudgetByFlag := func(flags []string) float64 {
|
||||
var total float64
|
||||
for _, budget := range budgets {
|
||||
if budget.Nonstock != nil {
|
||||
for _, nonstockFlag := range budget.Nonstock.Flags {
|
||||
flagName := strings.ToUpper(nonstockFlag.Name)
|
||||
for _, targetFlag := range flags {
|
||||
if flagName == strings.ToUpper(targetFlag) {
|
||||
total += budget.Price * budget.Qty
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// Budget per category
|
||||
budgetPakan := calculateBudgetByFlag([]string{"PAKAN", "PRE-STARTER", "STARTER", "FINISHER"})
|
||||
budgetOvk := calculateBudgetByFlag([]string{"OVK", "OBAT", "VITAMIN", "KIMIA"})
|
||||
budgetAyam := calculateBudgetByFlag([]string{"DOC", "PULLET", "LAYER"})
|
||||
budgetEkspedisi := calculateBudgetByFlag([]string{"EKSPEDISI"})
|
||||
|
||||
// Operational budget = total budget - pakan - ovk - ayam - ekspedisi
|
||||
totalBudgetAmount := 0.0
|
||||
for _, budget := range budgets {
|
||||
totalBudgetAmount += budget.Price * budget.Qty
|
||||
}
|
||||
budgetOperational := totalBudgetAmount - budgetPakan - budgetOvk - budgetAyam - budgetEkspedisi
|
||||
|
||||
|
||||
// Calculate realization totals
|
||||
var totalRealizationAmount float64
|
||||
var totalEkspedisiRealization float64
|
||||
for _, realization := range realizations {
|
||||
amount := realization.Price * realization.Qty
|
||||
totalRealizationAmount += amount
|
||||
|
||||
// Check if this is ekspedisi (need to check nonstock flags)
|
||||
if realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Nonstock != nil {
|
||||
for _, flag := range realization.ExpenseNonstock.Nonstock.Flags {
|
||||
if flag.Name == "EKSPEDISI" {
|
||||
totalEkspedisiRealization += amount
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalOperationalRealization := totalRealizationAmount - totalEkspedisiRealization
|
||||
|
||||
// Filter delivery products based on category
|
||||
var filteredDeliveryProducts []entity.MarketingDeliveryProduct
|
||||
for _, delivery := range deliveryProducts {
|
||||
// Get product from delivery
|
||||
if delivery.MarketingProduct.ProductWarehouse.Product.Id == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
product := delivery.MarketingProduct.ProductWarehouse.Product
|
||||
isEggProduct := false
|
||||
isChickenProduct := false
|
||||
|
||||
// Check product flags
|
||||
for _, flag := range product.Flags {
|
||||
flagName := strings.ToUpper(flag.Name)
|
||||
|
||||
// Egg product flags
|
||||
if flagName == "TELUR" || flagName == "TELURUTUH" || flagName == "TELURPECAH" ||
|
||||
flagName == "TELURPUTIH" || flagName == "TELURRETAK" {
|
||||
isEggProduct = true
|
||||
}
|
||||
|
||||
// Chicken product flags
|
||||
if flagName == "AYAMAFKIR" || flagName == "AYAMCULLING" || flagName == "AYAMMATI" {
|
||||
isChickenProduct = true
|
||||
}
|
||||
}
|
||||
|
||||
// Filter based on project flock category
|
||||
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
||||
// Laying: only egg products
|
||||
if isEggProduct {
|
||||
filteredDeliveryProducts = append(filteredDeliveryProducts, delivery)
|
||||
}
|
||||
} else {
|
||||
// Growing/Contract Growing: only chicken products
|
||||
if isChickenProduct || (!isEggProduct && !isChickenProduct) {
|
||||
// Include if chicken product or if no specific flags (default to chicken)
|
||||
filteredDeliveryProducts = append(filteredDeliveryProducts, delivery)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Calculate total weight sold and sales amount from filtered products
|
||||
var totalWeightSold float64
|
||||
var totalSalesAmount float64
|
||||
for _, delivery := range filteredDeliveryProducts {
|
||||
totalWeightSold += delivery.TotalWeight
|
||||
totalSalesAmount += delivery.TotalPrice
|
||||
}
|
||||
|
||||
|
||||
// Calculate metrics - always use kg ayam for rp_per_kg
|
||||
calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
|
||||
if actualPopulation > 0 {
|
||||
rpPerBird = amount / actualPopulation // Use actual population
|
||||
}
|
||||
if totalWeightProduced > 0 {
|
||||
rpPerKg = amount / totalWeightProduced
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate metrics for profit loss (use total population and total weight produced)
|
||||
calculateProfitLossMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
|
||||
if totalPopulation > 0 {
|
||||
rpPerBird = amount / totalPopulation
|
||||
}
|
||||
if totalWeightProduced > 0 {
|
||||
rpPerKg = amount / totalWeightProduced
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Build HPP Items using constants
|
||||
hppItems := []dto.HPPItem{}
|
||||
|
||||
// PAKAN item
|
||||
pakanBudgetRpPerBird, pakanBudgetRpPerKg := calculateMetrics(budgetPakan)
|
||||
pakanRealizationRpPerBird, pakanRealizationRpPerKg := calculateMetrics(totalPakanPrice)
|
||||
hppItems = append(hppItems, dto.ToHPPItem(
|
||||
1,
|
||||
"purchase",
|
||||
string(dto.HPPCodePakan),
|
||||
"Pembelian Pakan",
|
||||
dto.ToFinancialMetrics(pakanBudgetRpPerBird, pakanBudgetRpPerKg, budgetPakan),
|
||||
dto.ToFinancialMetrics(pakanRealizationRpPerBird, pakanRealizationRpPerKg, totalPakanPrice),
|
||||
))
|
||||
|
||||
// OVK item
|
||||
ovkBudgetRpPerBird, ovkBudgetRpPerKg := calculateMetrics(budgetOvk)
|
||||
ovkRealizationRpPerBird, ovkRealizationRpPerKg := calculateMetrics(totalOvkPrice)
|
||||
hppItems = append(hppItems, dto.ToHPPItem(
|
||||
2,
|
||||
"purchase",
|
||||
string(dto.HPPCodeOVK),
|
||||
"Pembelian OVK",
|
||||
dto.ToFinancialMetrics(ovkBudgetRpPerBird, ovkBudgetRpPerKg, budgetOvk),
|
||||
dto.ToFinancialMetrics(ovkRealizationRpPerBird, ovkRealizationRpPerKg, totalOvkPrice),
|
||||
))
|
||||
|
||||
// DOC/DEPRESIASI item
|
||||
docCode := string(dto.HPPCodeDOC)
|
||||
docLabel := "Pembelian DOC"
|
||||
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
||||
docCode = string(dto.HPPCodeDepresiasi)
|
||||
docLabel = "Depresiasi"
|
||||
}
|
||||
docBudgetRpPerBird, docBudgetRpPerKg := calculateMetrics(budgetAyam)
|
||||
docRealizationRpPerBird, docRealizationRpPerKg := calculateMetrics(totalAyamPrice)
|
||||
hppItems = append(hppItems, dto.ToHPPItem(
|
||||
3,
|
||||
"purchase",
|
||||
docCode,
|
||||
docLabel,
|
||||
dto.ToFinancialMetrics(docBudgetRpPerBird, docBudgetRpPerKg, budgetAyam),
|
||||
dto.ToFinancialMetrics(docRealizationRpPerBird, docRealizationRpPerKg, totalAyamPrice),
|
||||
))
|
||||
|
||||
// OVERHEAD item
|
||||
overheadBudgetRpPerBird, overheadBudgetRpPerKg := calculateMetrics(budgetOperational)
|
||||
overheadRealizationRpPerBird, overheadRealizationRpPerKg := calculateMetrics(totalOperationalRealization)
|
||||
hppItems = append(hppItems, dto.ToHPPItem(
|
||||
4,
|
||||
"overhead",
|
||||
string(dto.HPPCodeOverhead),
|
||||
"Pengeluaran Overhead",
|
||||
dto.ToFinancialMetrics(overheadBudgetRpPerBird, overheadBudgetRpPerKg, budgetOperational),
|
||||
dto.ToFinancialMetrics(overheadRealizationRpPerBird, overheadRealizationRpPerKg, totalOperationalRealization),
|
||||
))
|
||||
|
||||
// EKSPEDISI item
|
||||
ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg := calculateMetrics(budgetEkspedisi)
|
||||
ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg := calculateMetrics(totalEkspedisiRealization)
|
||||
hppItems = append(hppItems, dto.ToHPPItem(
|
||||
5,
|
||||
"overhead",
|
||||
string(dto.HPPCodeEkspedisi),
|
||||
"Beban Ekspedisi",
|
||||
dto.ToFinancialMetrics(ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg, budgetEkspedisi),
|
||||
dto.ToFinancialMetrics(ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg, totalEkspedisiRealization),
|
||||
))
|
||||
|
||||
// HPP Summary
|
||||
totalBudgetHpp := budgetPakan + budgetOvk + budgetAyam + budgetOperational + budgetEkspedisi
|
||||
totalRealizationHpp := totalPakanPrice + totalOvkPrice + totalAyamPrice + totalOperationalRealization + totalEkspedisiRealization
|
||||
|
||||
hppBudgetRpPerBird, hppBudgetRpPerKg := calculateMetrics(totalBudgetHpp)
|
||||
hppRealizationRpPerBird, hppRealizationRpPerKg := calculateMetrics(totalRealizationHpp)
|
||||
|
||||
var eggBudgeting, eggRealization *dto.FinancialMetrics
|
||||
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) && totalEggWeightKg > 0 {
|
||||
eggBudgetRpPerKg := totalBudgetHpp / totalEggWeightKg
|
||||
eggRealizationRpPerKg := totalRealizationHpp / totalEggWeightKg
|
||||
eggBudgeting = &dto.FinancialMetrics{
|
||||
RpPerBird: 0,
|
||||
RpPerKg: eggBudgetRpPerKg,
|
||||
Amount: totalBudgetHpp,
|
||||
}
|
||||
eggRealization = &dto.FinancialMetrics{
|
||||
RpPerBird: 0,
|
||||
RpPerKg: eggRealizationRpPerKg,
|
||||
Amount: totalRealizationHpp,
|
||||
}
|
||||
}
|
||||
|
||||
hppSummary := dto.ToHPPSummary(
|
||||
"HPP",
|
||||
dto.ToFinancialMetrics(hppBudgetRpPerBird, hppBudgetRpPerKg, totalBudgetHpp),
|
||||
dto.ToFinancialMetrics(hppRealizationRpPerBird, hppRealizationRpPerKg, totalRealizationHpp),
|
||||
eggBudgeting,
|
||||
eggRealization,
|
||||
)
|
||||
|
||||
hppSection := dto.ToHPPSection(hppItems, hppSummary)
|
||||
|
||||
// Build Profit Loss Items using constants
|
||||
plItems := []dto.ProfitLossItem{}
|
||||
|
||||
// SALES item
|
||||
salesRpPerBird, salesRpPerKg := calculateProfitLossMetrics(totalSalesAmount)
|
||||
salesLabel := "Penjualan Ayam"
|
||||
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
||||
salesLabel = "Penjualan Telur"
|
||||
}
|
||||
plItems = append(plItems, dto.ToProfitLossItem(
|
||||
string(dto.PLCodeSales),
|
||||
salesLabel,
|
||||
"income",
|
||||
salesRpPerBird,
|
||||
salesRpPerKg,
|
||||
totalSalesAmount,
|
||||
))
|
||||
|
||||
// SAPRONAK item - combines DOC/Depresiasi + PAKAN + OVK
|
||||
totalSapronakAmount := totalAyamPrice + totalPakanPrice + totalOvkPrice
|
||||
sapronakRpPerBird := docRealizationRpPerBird + pakanRealizationRpPerBird + ovkRealizationRpPerBird
|
||||
sapronakRpPerKg := docRealizationRpPerKg + pakanRealizationRpPerKg + ovkRealizationRpPerKg
|
||||
sapronakLabel := "Pengeluaran Sapronak"
|
||||
plItems = append(plItems, dto.ToProfitLossItem(
|
||||
string(dto.PLCodeSapronak),
|
||||
sapronakLabel,
|
||||
"purchase",
|
||||
sapronakRpPerBird,
|
||||
sapronakRpPerKg,
|
||||
totalSapronakAmount,
|
||||
))
|
||||
|
||||
// OVERHEAD item
|
||||
overheadRpPerBird, overheadRpPerKg := calculateMetrics(totalOperationalRealization)
|
||||
plItems = append(plItems, dto.ToProfitLossItem(
|
||||
string(dto.PLCodeOverhead),
|
||||
"Overhead",
|
||||
"overhead",
|
||||
overheadRpPerBird,
|
||||
overheadRpPerKg,
|
||||
totalOperationalRealization,
|
||||
))
|
||||
|
||||
// EKSPEDISI item
|
||||
plItems = append(plItems, dto.ToProfitLossItem(
|
||||
string(dto.PLCodeEkspedisi),
|
||||
"Ekspedisi",
|
||||
"overhead",
|
||||
ekspedisiRealizationRpPerBird,
|
||||
ekspedisiRealizationRpPerKg,
|
||||
totalEkspedisiRealization,
|
||||
))
|
||||
|
||||
// Profit Loss Summary
|
||||
// Gross Profit = Sales - (DOC + PAKAN + OVK) only
|
||||
// Gross Profit should NOT include overhead and ekspedisi
|
||||
costOfGoodsSold := totalAyamPrice + totalPakanPrice + totalOvkPrice
|
||||
costOfGoodsSoldRpPerBird := sapronakRpPerBird
|
||||
|
||||
grossProfit := totalSalesAmount - costOfGoodsSold
|
||||
grossProfitRpPerBird := salesRpPerBird - costOfGoodsSoldRpPerBird
|
||||
|
||||
// Operating Expenses (Overhead + Ekspedisi)
|
||||
totalOperatingExpenses := totalOperationalRealization + totalEkspedisiRealization
|
||||
totalOperatingExpensesRpPerBird := overheadRpPerBird + ekspedisiRealizationRpPerBird
|
||||
|
||||
// Net Profit = Gross Profit - Operating Expenses
|
||||
netProfit := grossProfit - totalOperatingExpenses
|
||||
netProfitRpPerBird := grossProfitRpPerBird - totalOperatingExpensesRpPerBird
|
||||
|
||||
plSummary := dto.ToProfitLossSummary(
|
||||
dto.ToFinancialMetrics(grossProfitRpPerBird, 0, grossProfit),
|
||||
dto.ToFinancialMetrics(totalOperatingExpensesRpPerBird, 0, totalOperatingExpenses),
|
||||
dto.ToFinancialMetrics(netProfitRpPerBird, 0, netProfit),
|
||||
)
|
||||
|
||||
profitLossSection := dto.ToProfitLossSection(plItems, plSummary)
|
||||
|
||||
// Build complete response
|
||||
data := dto.ToClosingKeuanganData(hppSection, profitLossSection)
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
// containsItem checks if a string exists in a slice
|
||||
func containsItem(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if strings.EqualFold(s, item) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -2,8 +2,8 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
@@ -112,7 +112,7 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
|
||||
}
|
||||
|
||||
// We no longer filter by date for closing sapronak report; pass nil pointers.
|
||||
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, nil, nil, params.Flag)
|
||||
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, params.Flag)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report")
|
||||
@@ -126,8 +126,6 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
|
||||
KandangName: pfk.Kandang.Name,
|
||||
Period: pfk.Period,
|
||||
Status: status,
|
||||
StartDate: nil,
|
||||
EndDate: nil,
|
||||
TotalIncomingValue: totalIncoming,
|
||||
TotalUsageValue: totalUsage,
|
||||
Items: items,
|
||||
@@ -265,6 +263,7 @@ type sapronakDetailMaps struct {
|
||||
AdjOutgoing map[uint][]dto.SapronakDetailDTO
|
||||
TransferIn map[uint][]dto.SapronakDetailDTO
|
||||
TransferOut map[uint][]dto.SapronakDetailDTO
|
||||
SalesOut map[uint][]dto.SapronakDetailDTO
|
||||
}
|
||||
|
||||
func buildSapronakDetails(
|
||||
@@ -274,6 +273,7 @@ func buildSapronakDetails(
|
||||
adjOutgoingRows map[uint][]repository.SapronakDetailRow,
|
||||
transferInRows map[uint][]repository.SapronakDetailRow,
|
||||
transferOutRows map[uint][]repository.SapronakDetailRow,
|
||||
salesOutRows map[uint][]repository.SapronakDetailRow,
|
||||
) sapronakDetailMaps {
|
||||
result := sapronakDetailMaps{
|
||||
Incoming: make(map[uint][]dto.SapronakDetailDTO),
|
||||
@@ -282,6 +282,7 @@ func buildSapronakDetails(
|
||||
AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO),
|
||||
TransferIn: make(map[uint][]dto.SapronakDetailDTO),
|
||||
TransferOut: make(map[uint][]dto.SapronakDetailDTO),
|
||||
SalesOut: make(map[uint][]dto.SapronakDetailDTO),
|
||||
}
|
||||
|
||||
addRows := func(target map[uint][]dto.SapronakDetailDTO, src map[uint][]repository.SapronakDetailRow, jenis string, masuk bool) {
|
||||
@@ -314,11 +315,12 @@ func buildSapronakDetails(
|
||||
addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false)
|
||||
addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true)
|
||||
addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false)
|
||||
addRows(result.SalesOut, salesOutRows, "Penjualan", false)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, start, end *time.Time, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
|
||||
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
|
||||
// For sapronak closing report we intentionally ignore date range
|
||||
// and aggregate all historical transactions for the kandang/project.
|
||||
incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId)
|
||||
@@ -353,6 +355,10 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
salesOutRows, err := s.Repository.FetchSapronakSales(ctx, pfk.KandangId)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
|
||||
filterFlag := strings.ToUpper(strings.TrimSpace(flagFilter))
|
||||
matchesFlag := func(f string) bool {
|
||||
@@ -365,6 +371,34 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
||||
}
|
||||
return candidate == filterFlag
|
||||
}
|
||||
dedupTransfers := func(src map[uint][]dto.SapronakDetailDTO) map[uint][]dto.SapronakDetailDTO {
|
||||
result := make(map[uint][]dto.SapronakDetailDTO, len(src))
|
||||
seen := make(map[string]struct{})
|
||||
for pid, rows := range src {
|
||||
for _, d := range rows {
|
||||
dateKey := ""
|
||||
if d.Tanggal != nil {
|
||||
dateKey = d.Tanggal.Format("2006-01-02")
|
||||
}
|
||||
qtyKey := d.QtyMasuk
|
||||
if qtyKey == 0 {
|
||||
qtyKey = d.QtyKeluar
|
||||
}
|
||||
|
||||
ref := strings.TrimSpace(d.NoReferensi)
|
||||
key := fmt.Sprintf("%d|%s|%s|%.3f", pid, ref, dateKey, qtyKey)
|
||||
if ref == "" {
|
||||
key = fmt.Sprintf("%d|%s|%s|%.3f|%s", pid, ref, dateKey, qtyKey, strings.ToUpper(strings.TrimSpace(d.Flag)))
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
result[pid] = append(result[pid], d)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// For project flocks with category GROWING, pullet usage from chickin
|
||||
// should not be counted yet. Only when category is LAYING we allow
|
||||
@@ -403,13 +437,17 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
||||
usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...)
|
||||
}
|
||||
|
||||
detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows)
|
||||
detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows, salesOutRows)
|
||||
incomingDetails := detailMaps.Incoming
|
||||
usageDetails := detailMaps.Usage
|
||||
adjIncoming := detailMaps.AdjIncoming
|
||||
adjOutgoing := detailMaps.AdjOutgoing
|
||||
transIncoming := detailMaps.TransferIn
|
||||
transOutgoing := detailMaps.TransferOut
|
||||
salesOutgoing := detailMaps.SalesOut
|
||||
|
||||
transIncoming = dedupTransfers(transIncoming)
|
||||
transOutgoing = dedupTransfers(transOutgoing)
|
||||
|
||||
ensureGroup := func(flag string) *dto.SapronakGroupDTO {
|
||||
if g, ok := groupMap[flag]; ok {
|
||||
@@ -419,6 +457,22 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
||||
return groupMap[flag]
|
||||
}
|
||||
|
||||
resolveFlagName := func(productID uint, details []dto.SapronakDetailDTO) (string, string) {
|
||||
flag := ""
|
||||
name := ""
|
||||
if item, ok := itemMap[productID]; ok {
|
||||
flag = item.Flag
|
||||
name = item.ProductName
|
||||
}
|
||||
if flag == "" && len(details) > 0 {
|
||||
flag = details[0].Flag
|
||||
}
|
||||
if name == "" && len(details) > 0 {
|
||||
name = details[0].ProductName
|
||||
}
|
||||
return flag, name
|
||||
}
|
||||
|
||||
for _, row := range incoming {
|
||||
if !matchesFlag(row.Flag) {
|
||||
continue
|
||||
@@ -554,19 +608,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
||||
}
|
||||
|
||||
for productID, details := range incomingDetails {
|
||||
flag := ""
|
||||
name := ""
|
||||
if item, ok := itemMap[productID]; ok {
|
||||
flag = item.Flag
|
||||
name = item.ProductName
|
||||
}
|
||||
flag, name := resolveFlagName(productID, details)
|
||||
if !matchesFlag(flag) {
|
||||
continue
|
||||
}
|
||||
group := ensureGroup(flag)
|
||||
for _, d := range details {
|
||||
d.Flag = flag
|
||||
d.ProductName = name
|
||||
if d.Flag == "" {
|
||||
d.Flag = flag
|
||||
}
|
||||
if d.ProductName == "" {
|
||||
d.ProductName = name
|
||||
}
|
||||
group.Items = append(group.Items, d)
|
||||
group.TotalMasuk += d.QtyMasuk
|
||||
group.TotalNilai += d.Nilai
|
||||
@@ -575,19 +628,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
||||
}
|
||||
|
||||
for productID, details := range adjIncoming {
|
||||
flag := ""
|
||||
name := ""
|
||||
if item, ok := itemMap[productID]; ok {
|
||||
flag = item.Flag
|
||||
name = item.ProductName
|
||||
}
|
||||
flag, name := resolveFlagName(productID, details)
|
||||
if !matchesFlag(flag) {
|
||||
continue
|
||||
}
|
||||
group := ensureGroup(flag)
|
||||
for _, d := range details {
|
||||
d.Flag = flag
|
||||
d.ProductName = name
|
||||
if d.Flag == "" {
|
||||
d.Flag = flag
|
||||
}
|
||||
if d.ProductName == "" {
|
||||
d.ProductName = name
|
||||
}
|
||||
group.Items = append(group.Items, d)
|
||||
group.TotalMasuk += d.QtyMasuk
|
||||
group.TotalNilai += d.Nilai
|
||||
@@ -596,19 +648,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
||||
}
|
||||
|
||||
for productID, details := range usageDetails {
|
||||
flag := ""
|
||||
name := ""
|
||||
if item, ok := itemMap[productID]; ok {
|
||||
flag = item.Flag
|
||||
name = item.ProductName
|
||||
}
|
||||
flag, name := resolveFlagName(productID, details)
|
||||
if !matchesFlag(flag) {
|
||||
continue
|
||||
}
|
||||
group := ensureGroup(flag)
|
||||
for _, d := range details {
|
||||
d.Flag = flag
|
||||
d.ProductName = name
|
||||
if d.Flag == "" {
|
||||
d.Flag = flag
|
||||
}
|
||||
if d.ProductName == "" {
|
||||
d.ProductName = name
|
||||
}
|
||||
group.Items = append(group.Items, d)
|
||||
group.TotalKeluar += d.QtyKeluar
|
||||
group.SaldoAkhir -= d.QtyKeluar
|
||||
@@ -616,19 +667,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
||||
}
|
||||
|
||||
for productID, details := range adjOutgoing {
|
||||
flag := ""
|
||||
name := ""
|
||||
if item, ok := itemMap[productID]; ok {
|
||||
flag = item.Flag
|
||||
name = item.ProductName
|
||||
}
|
||||
flag, name := resolveFlagName(productID, details)
|
||||
if !matchesFlag(flag) {
|
||||
continue
|
||||
}
|
||||
group := ensureGroup(flag)
|
||||
for _, d := range details {
|
||||
d.Flag = flag
|
||||
d.ProductName = name
|
||||
if d.Flag == "" {
|
||||
d.Flag = flag
|
||||
}
|
||||
if d.ProductName == "" {
|
||||
d.ProductName = name
|
||||
}
|
||||
group.Items = append(group.Items, d)
|
||||
group.TotalKeluar += d.QtyKeluar
|
||||
group.SaldoAkhir -= d.QtyKeluar
|
||||
@@ -636,19 +686,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
||||
}
|
||||
|
||||
for productID, details := range transIncoming {
|
||||
flag := ""
|
||||
name := ""
|
||||
if item, ok := itemMap[productID]; ok {
|
||||
flag = item.Flag
|
||||
name = item.ProductName
|
||||
}
|
||||
flag, name := resolveFlagName(productID, details)
|
||||
if !matchesFlag(flag) {
|
||||
continue
|
||||
}
|
||||
group := ensureGroup(flag)
|
||||
for _, d := range details {
|
||||
d.Flag = flag
|
||||
d.ProductName = name
|
||||
if d.Flag == "" {
|
||||
d.Flag = flag
|
||||
}
|
||||
if d.ProductName == "" {
|
||||
d.ProductName = name
|
||||
}
|
||||
group.Items = append(group.Items, d)
|
||||
group.TotalMasuk += d.QtyMasuk
|
||||
group.TotalNilai += d.Nilai
|
||||
@@ -657,19 +706,37 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
||||
}
|
||||
|
||||
for productID, details := range transOutgoing {
|
||||
flag := ""
|
||||
name := ""
|
||||
if item, ok := itemMap[productID]; ok {
|
||||
flag = item.Flag
|
||||
name = item.ProductName
|
||||
}
|
||||
flag, name := resolveFlagName(productID, details)
|
||||
if !matchesFlag(flag) {
|
||||
continue
|
||||
}
|
||||
group := ensureGroup(flag)
|
||||
for _, d := range details {
|
||||
d.Flag = flag
|
||||
d.ProductName = name
|
||||
if d.Flag == "" {
|
||||
d.Flag = flag
|
||||
}
|
||||
if d.ProductName == "" {
|
||||
d.ProductName = name
|
||||
}
|
||||
group.Items = append(group.Items, d)
|
||||
group.TotalKeluar += d.QtyKeluar
|
||||
group.SaldoAkhir -= d.QtyKeluar
|
||||
}
|
||||
}
|
||||
|
||||
for productID, details := range salesOutgoing {
|
||||
flag, name := resolveFlagName(productID, details)
|
||||
if !matchesFlag(flag) {
|
||||
continue
|
||||
}
|
||||
group := ensureGroup(flag)
|
||||
for _, d := range details {
|
||||
if d.Flag == "" {
|
||||
d.Flag = flag
|
||||
}
|
||||
if d.ProductName == "" {
|
||||
d.ProductName = name
|
||||
}
|
||||
group.Items = append(group.Items, d)
|
||||
group.TotalKeluar += d.QtyKeluar
|
||||
group.SaldoAkhir -= d.QtyKeluar
|
||||
|
||||
@@ -20,7 +20,9 @@ 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"`
|
||||
Search string `query:"search" validate:"omitempty,max=100"`
|
||||
}
|
||||
|
||||
@@ -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,51 +13,51 @@ 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)
|
||||
route.Get("/", m.RequirePermissions(m.P_DailyChecklistGetAll), ctrl.GetAll)
|
||||
route.Get("/report", m.RequirePermissions(m.P_DailyChecklistReports), ctrl.GetReport)
|
||||
|
||||
route.Get("/summary", ctrl.GetSummary)
|
||||
route.Get("/summary", m.RequirePermissions(m.P_DailyChecklistDashboardList), ctrl.GetSummary)
|
||||
|
||||
route.Get("/report", ctrl.GetReport)
|
||||
// route.Get("/report", ctrl.GetReport)
|
||||
|
||||
// create daily checklist
|
||||
route.Post("/", ctrl.CreateOne)
|
||||
// upsert daily checklist
|
||||
route.Post("/", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.CreateOne)
|
||||
|
||||
// get detail data daily checklist by id
|
||||
route.Get("/relation/:idDailyChecklist", ctrl.GetOne)
|
||||
route.Get("/relation/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistGetOne), ctrl.GetOne)
|
||||
|
||||
// get phases by daily checklist id
|
||||
route.Get("/phase/:idDailyChecklist", ctrl.GetPhaseByIdChecklist)
|
||||
route.Get("/phase/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.GetPhaseByIdChecklist)
|
||||
|
||||
// create task
|
||||
/*
|
||||
ketika add phase
|
||||
*/
|
||||
route.Post("/phase/:idDailyChecklist", ctrl.CreateDailyChecklistPhase)
|
||||
route.Post("/phase/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.CreateDailyChecklistPhase)
|
||||
|
||||
// create assigment
|
||||
/*
|
||||
ketika add ABK
|
||||
*/
|
||||
route.Post("/assignment/:idDailyChecklist", ctrl.CreateAssignment)
|
||||
route.Post("/assignment/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.CreateAssignment)
|
||||
|
||||
// remove assignment
|
||||
/*
|
||||
ketika remove ABK
|
||||
*/
|
||||
route.Delete("/:idDailyChecklist/assignments/:idEmployee", ctrl.RemoveAssignment)
|
||||
route.Delete("/:idDailyChecklist/assignments/:idEmployee", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.RemoveAssignment)
|
||||
|
||||
//get all tasks
|
||||
route.Get("/tasks", ctrl.GetAllTasks)
|
||||
route.Get("/tasks", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.GetAllTasks)
|
||||
|
||||
// update assignment
|
||||
/*
|
||||
ketika check dan uncheck tugas oleh ABK
|
||||
*/
|
||||
route.Post("/assignment", ctrl.UpdateAssignment)
|
||||
route.Post("/assignment", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.UpdateAssignment)
|
||||
|
||||
route.Patch("/:idDailyChecklist", ctrl.UpdateOne)
|
||||
route.Delete("/:idDailyChecklist", ctrl.DeleteOne)
|
||||
route.Patch("/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.UpdateOne)
|
||||
route.Delete("/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.DeleteOne)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -46,7 +52,7 @@ type SummaryQuery struct {
|
||||
|
||||
type ReportQuery struct {
|
||||
Page int `query:"page" validate:"required,number,min=1,gt=0"`
|
||||
Limit int `query:"limit" validate:"required,number,min=1,max=100,gt=0"`
|
||||
Limit int `query:"limit" validate:"required,number,min=1,gt=0"`
|
||||
Month int `query:"bulan" validate:"required,number,min=1,max=12"`
|
||||
Year int `query:"tahun" validate:"required,number,min=1900"`
|
||||
AreaID *uint `query:"area_id" validate:"omitempty"`
|
||||
|
||||
@@ -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("expenses.category LIKE ? OR expenses.reference_number LIKE ? OR expenses.po_number LIKE ? OR expenses.notes LIKE ? OR suppliers.name LIKE ?",
|
||||
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("category LIKE ?", "%"+params.Search+"%")
|
||||
return db.Where("category ILIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
@@ -396,6 +396,10 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
updateBody["supplier_id"] = *req.SupplierID
|
||||
}
|
||||
|
||||
if req.Notes != nil {
|
||||
updateBody["notes"] = *req.Notes
|
||||
}
|
||||
|
||||
if req.LocationID != nil {
|
||||
locationID := uint(*req.LocationID)
|
||||
updateBody["location_id"] = locationID
|
||||
@@ -568,20 +572,28 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||
}
|
||||
if *latestApproval.Action != entity.ApprovalActionUpdated {
|
||||
|
||||
if *latestApproval.Action != entity.ApprovalActionUpdated && latestApproval.StepNumber > uint16(utils.ExpenseStepPengajuan) {
|
||||
|
||||
approvalAction := entity.ApprovalActionUpdated
|
||||
|
||||
previousStep := approvalutils.ApprovalStep(latestApproval.StepNumber) - 1
|
||||
|
||||
if previousStep < utils.ExpenseStepPengajuan {
|
||||
previousStep = utils.ExpenseStepPengajuan
|
||||
}
|
||||
|
||||
if _, err := approvalSvcTx.CreateApproval(
|
||||
c.Context(),
|
||||
utils.ApprovalWorkflowExpense,
|
||||
id,
|
||||
utils.ExpenseStepPengajuan,
|
||||
previousStep,
|
||||
&approvalAction,
|
||||
actorID,
|
||||
nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval step")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if s.DocumentSvc != nil && len(req.Documents) > 0 {
|
||||
@@ -1049,21 +1061,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))
|
||||
|
||||
@@ -31,6 +31,7 @@ type Update struct {
|
||||
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"`
|
||||
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"`
|
||||
LocationID *uint64 `form:"location_id" json:"location_id" validate:"omitempty,gt=0"`
|
||||
Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"`
|
||||
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
|
||||
}
|
||||
|
||||
+2
-2
@@ -399,11 +399,11 @@ func (r *ProductWarehouseRepositoryImpl) ListProductIDsByFlagPrefixes(ctx contex
|
||||
}
|
||||
like := prefix + "%"
|
||||
if !applied {
|
||||
db = db.Where("flags.name LIKE ?", like)
|
||||
db = db.Where("flags.name ILIKE ?", like)
|
||||
applied = true
|
||||
continue
|
||||
}
|
||||
db = db.Or("flags.name LIKE ?", like)
|
||||
db = db.Or("flags.name ILIKE ?", like)
|
||||
}
|
||||
|
||||
if visibleStatus != nil {
|
||||
|
||||
@@ -4,20 +4,17 @@ import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
)
|
||||
|
||||
type TransferRelationDTO struct {
|
||||
Id uint64 `json:"id"`
|
||||
TransferReason string `json:"transfer_reason"`
|
||||
TransferDate string `json:"transfer_date"`
|
||||
SourceWarehouse *WarehouseDetailDTO `json:"source_warehouse,omitempty"`
|
||||
DestinationWarehouse *WarehouseDetailDTO `json:"destination_warehouse,omitempty"`
|
||||
}
|
||||
|
||||
type WarehouseSimpleDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Id uint64 `json:"id"`
|
||||
MovementNumber string `json:"movement_number"`
|
||||
TransferReason string `json:"transfer_reason"`
|
||||
TransferDate string `json:"transfer_date"`
|
||||
SourceWarehouse *warehouseDTO.WarehouseRelationDTO `json:"source_warehouse,omitempty"`
|
||||
DestinationWarehouse *warehouseDTO.WarehouseRelationDTO `json:"destination_warehouse,omitempty"`
|
||||
}
|
||||
|
||||
type ProductSimpleDTO struct {
|
||||
@@ -25,16 +22,6 @@ type ProductSimpleDTO struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type AreaDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type LocationDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type SupplierSimpleDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -48,13 +35,6 @@ type DocumentDTO struct {
|
||||
Size float64 `json:"size"`
|
||||
}
|
||||
|
||||
type WarehouseDetailDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Location *LocationDTO `json:"location"`
|
||||
Area *AreaDTO `json:"area"`
|
||||
}
|
||||
|
||||
type TransferListDTO struct {
|
||||
TransferRelationDTO
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||
@@ -97,16 +77,19 @@ type TransferDeliveryItemDTO struct {
|
||||
}
|
||||
|
||||
func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
|
||||
var sourceWarehouse *WarehouseDetailDTO
|
||||
var sourceWarehouse *warehouseDTO.WarehouseRelationDTO
|
||||
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
|
||||
sourceWarehouse = toWarehouseDetailDTO(e.FromWarehouse)
|
||||
mapped := warehouseDTO.ToWarehouseRelationDTO(*e.FromWarehouse)
|
||||
sourceWarehouse = &mapped
|
||||
}
|
||||
var destinationWarehouse *WarehouseDetailDTO
|
||||
var destinationWarehouse *warehouseDTO.WarehouseRelationDTO
|
||||
if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 {
|
||||
destinationWarehouse = toWarehouseDetailDTO(e.ToWarehouse)
|
||||
mapped := warehouseDTO.ToWarehouseRelationDTO(*e.ToWarehouse)
|
||||
destinationWarehouse = &mapped
|
||||
}
|
||||
return TransferRelationDTO{
|
||||
Id: e.Id,
|
||||
MovementNumber: e.MovementNumber,
|
||||
TransferReason: e.Reason,
|
||||
TransferDate: e.CreatedAt.Format("2006-01-02"),
|
||||
SourceWarehouse: sourceWarehouse,
|
||||
@@ -114,38 +97,6 @@ func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
|
||||
}
|
||||
}
|
||||
|
||||
func toAreaDTO(a *entity.Area) *AreaDTO {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
return &AreaDTO{
|
||||
Id: a.Id,
|
||||
Name: a.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func toLocationDTO(l *entity.Location) *LocationDTO {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return &LocationDTO{
|
||||
Id: l.Id,
|
||||
Name: l.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func toWarehouseDetailDTO(w *entity.Warehouse) *WarehouseDetailDTO {
|
||||
if w == nil {
|
||||
return nil
|
||||
}
|
||||
return &WarehouseDetailDTO{
|
||||
Id: w.Id,
|
||||
Name: w.Name,
|
||||
Location: toLocationDTO(w.Location),
|
||||
Area: toAreaDTO(&w.Area),
|
||||
}
|
||||
}
|
||||
|
||||
func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
|
||||
var createdUser *userDTO.UserRelationDTO
|
||||
if e.CreatedUser != nil {
|
||||
|
||||
@@ -40,6 +40,6 @@ func (r *StockTransferRepositoryImpl) GenerateMovementNumber(ctx context.Context
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
movementNumber := fmt.Sprintf("ST-%05d", seq)
|
||||
movementNumber := fmt.Sprintf("PND-LTI-%05d", seq)
|
||||
return movementNumber, nil
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ func TransferRoutes(v1 fiber.Router, u user.UserService, s transfer.TransferServ
|
||||
route := v1.Group("/transfers")
|
||||
route.Use(m.Auth(u))
|
||||
|
||||
route.Get("/",m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll)
|
||||
route.Post("/",m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne)
|
||||
route.Get("/:id",m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne)
|
||||
route.Get("/", m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll)
|
||||
route.Post("/", m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne)
|
||||
route.Get("/:id", m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne)
|
||||
|
||||
}
|
||||
|
||||
@@ -99,7 +99,11 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
|
||||
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
if params.Search != "" {
|
||||
db = db.Where("movement_number LIKE ?", "%"+strings.TrimSpace(params.Search)+"%")
|
||||
searchTerm := "%" + strings.TrimSpace(params.Search) + "%"
|
||||
db = db.Joins("LEFT JOIN warehouses AS from_warehouses ON from_warehouses.id = stock_transfers.from_warehouse_id").
|
||||
Joins("LEFT JOIN warehouses AS to_warehouses ON to_warehouses.id = stock_transfers.to_warehouse_id").
|
||||
Where("movement_number ILIKE ? OR from_warehouses.name ILIKE ? OR to_warehouses.name ILIKE ?",
|
||||
searchTerm, searchTerm, searchTerm)
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
@@ -118,9 +122,9 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Transfer dengan ID %d tidak ditemukan", id))
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer")
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data transfer dengan ID %d", id))
|
||||
}
|
||||
|
||||
return transferPtr, nil
|
||||
@@ -136,12 +140,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID))
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk dengan ID %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID))
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek stok produk di gudang asal")
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengecek stok produk %d di gudang asal", product.ProductID))
|
||||
}
|
||||
if sourcePW.Quantity < product.ProductQty {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID))
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak mencukupi. Tersedia: %.2f, Diminta: %.2f", product.ProductID, sourcePW.Quantity, product.ProductQty))
|
||||
}
|
||||
pwIDs = append(pwIDs, sourcePW.Id)
|
||||
}
|
||||
@@ -161,10 +165,10 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
|
||||
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock untuk gudang tujuan")
|
||||
}
|
||||
if projectFlockKandang.ClosedAt != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock tujuan sudah closing")
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Project flock untuk gudang tujuan sudah ditutup (closing) pada %s", projectFlockKandang.ClosedAt.Format("2006-01-02")))
|
||||
}
|
||||
|
||||
actorID, err := m.ActorIDFromContext(c)
|
||||
@@ -192,16 +196,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d tidak ditemukan", delivery.SupplierID))
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek data supplier")
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data supplier dengan ID %d", delivery.SupplierID))
|
||||
}
|
||||
if supplier.Category != string(utils.SupplierCategoryBOP) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d bukan kategori BOP", delivery.SupplierID))
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier '%s' (ID: %d) bukan kategori BOP. Kategori saat ini: %s", supplier.Name, delivery.SupplierID, supplier.Category))
|
||||
}
|
||||
}
|
||||
|
||||
movementNumber, err := s.StockTransferRepo.GenerateMovementNumber(c.Context())
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number")
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat nomor movement transfer")
|
||||
}
|
||||
|
||||
transferDate, _ := utils.ParseDateString(req.TransferDate)
|
||||
@@ -239,16 +243,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID))
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID))
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse source")
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data product warehouse untuk produk %d di gudang asal", product.ProductID))
|
||||
}
|
||||
|
||||
destPW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID(
|
||||
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
|
||||
)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse destination")
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data product warehouse untuk produk %d di gudang tujuan", product.ProductID))
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
ctx := c.Context()
|
||||
@@ -256,14 +260,21 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set ProjectFlockKandangId hanya jika ada kandang
|
||||
var pfkID *uint
|
||||
if projectFlockKandangID > 0 {
|
||||
pfkID = &projectFlockKandangID
|
||||
}
|
||||
|
||||
destPW = &entity.ProductWarehouse{
|
||||
ProductId: uint(product.ProductID),
|
||||
WarehouseId: uint(req.DestinationWarehouseID),
|
||||
Quantity: 0,
|
||||
ProjectFlockKandangId: &projectFlockKandangID,
|
||||
ProjectFlockKandangId: pfkID,
|
||||
}
|
||||
if err := productWarehouseRepoTX.CreateOne(c.Context(), destPW, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat product warehouse destination")
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal membuat product warehouse untuk produk %d di gudang tujuan", product.ProductID))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,7 +320,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
for _, prod := range item.Products {
|
||||
detail, ok := detailMap[uint64(prod.ProductID)]
|
||||
if !ok {
|
||||
return fmt.Errorf("produk %d tidak ditemukan di detail", prod.ProductID)
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan dalam daftar transfer untuk delivery #%d", prod.ProductID, i+1))
|
||||
}
|
||||
deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{
|
||||
StockTransferDeliveryId: delivery.Id,
|
||||
@@ -372,7 +383,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak cukup di gudang asal untuk produk %d: %v", product.ProductID, err))
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal. Error: %v", product.ProductID, err))
|
||||
}
|
||||
|
||||
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||
@@ -381,7 +392,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
"usage_qty": consumeResult.UsageQuantity,
|
||||
"pending_qty": consumeResult.PendingQuantity,
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("gagal update usage tracking: %w", err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengupdate tracking usage untuk produk %d", product.ProductID))
|
||||
}
|
||||
|
||||
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
|
||||
@@ -394,7 +405,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal menambah stok di gudang tujuan untuk produk %d: %v", product.ProductID, err))
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal menambah stok untuk produk %d di gudang tujuan. Error: %v", product.ProductID, err))
|
||||
}
|
||||
|
||||
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||
@@ -402,7 +413,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
Updates(map[string]interface{}{
|
||||
"total_qty": replenishResult.AddedQuantity,
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("gagal update total tracking: %w", err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengupdate tracking total untuk produk %d", product.ProductID))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,7 +447,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to process transfer transaction: %v", err))
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal memproses transfer. Error: %v", err))
|
||||
}
|
||||
|
||||
result, err := s.GetOne(c, uint(entityTransfer.Id))
|
||||
@@ -446,8 +457,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
|
||||
if len(expensePayloads) > 0 {
|
||||
if err := s.notifyExpenseItemsDelivered(c, entityTransfer.Id, expensePayloads); err != nil {
|
||||
s.Log.Errorf("Failed to sync expense for transfer %d: %+v", entityTransfer.Id, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to sync expense: %v", err))
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal sinkronisasi data expense untuk transfer %s. Silakan cek manual di module expense", entityTransfer.MovementNumber))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,32 +471,26 @@ func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID u
|
||||
return s.ExpenseBridge.OnItemsDelivered(c, transferID, payloads)
|
||||
}
|
||||
|
||||
func (s *transferService) notifyExpenseDetailsDeleted(ctx context.Context, transferID uint64, items []entity.StockTransferDetail) error {
|
||||
if s.ExpenseBridge == nil || transferID == 0 || len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.ExpenseBridge.OnItemsDeleted(ctx, transferID, items)
|
||||
}
|
||||
|
||||
func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
|
||||
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
|
||||
}
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang")
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data gudang dengan ID %d", warehouseID))
|
||||
}
|
||||
|
||||
// Jika warehouse tidak punya kandang_id, return 0 tanpa error
|
||||
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID))
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId))
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak ada project flock aktif untuk kandang %d", *warehouse.KandangId))
|
||||
}
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang")
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock kandang yang aktif")
|
||||
}
|
||||
|
||||
return uint(projectFlockKandang.Id), nil
|
||||
|
||||
@@ -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
|
||||
@@ -144,12 +140,21 @@ func (b *transferExpenseBridge) markExpensesUpdated(ctx context.Context, expense
|
||||
if actorID == 0 {
|
||||
actorID = 1
|
||||
}
|
||||
svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
||||
action := entity.ApprovalActionUpdated
|
||||
approvalRepo := commonRepo.NewApprovalRepository(b.db)
|
||||
svc := commonSvc.NewApprovalService(approvalRepo)
|
||||
action := entity.ApprovalActionCreated
|
||||
|
||||
for id := range expenseIDs {
|
||||
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||
latestApproval, err := approvalRepo.LatestByTarget(ctx, string(utils.ApprovalWorkflowExpense), uint(id), nil)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
if latestApproval == nil {
|
||||
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -220,7 +225,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 +232,7 @@ func (b *transferExpenseBridge) createExpenseViaService(
|
||||
|
||||
costItems = append(costItems, expenseValidation.CostItem{
|
||||
NonstockID: expeditionNonstockID,
|
||||
Quantity: 1,
|
||||
Quantity: 1,
|
||||
Price: price,
|
||||
Notes: note,
|
||||
})
|
||||
@@ -251,14 +255,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 +334,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 +353,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 +399,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,28 +137,41 @@ 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.AreaId > 0 || filters.LocationId > 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")
|
||||
}
|
||||
|
||||
if filters.WarehouseId > 0 {
|
||||
if filters.WarehouseId > 0 || filters.Search != "" {
|
||||
db = db.Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouses.warehouse_id")
|
||||
}
|
||||
|
||||
if filters.Search != "" {
|
||||
db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id")
|
||||
}
|
||||
db = db.Joins("LEFT JOIN users AS sales_users ON sales_users.id = marketings.sales_person_id")
|
||||
|
||||
if filters.Search != "" {
|
||||
searchPattern := "%" + filters.Search + "%"
|
||||
db = db.Where("marketing_delivery_products.vehicle_number ILIKE ? OR marketings.so_number ILIKE ? OR customers.name ILIKE ? OR products.name ILIKE ?",
|
||||
searchPattern, searchPattern, searchPattern, searchPattern)
|
||||
db = db.Where(`(
|
||||
marketing_delivery_products.vehicle_number ILIKE ? OR
|
||||
customers.name ILIKE ? OR
|
||||
warehouses.name ILIKE ? OR
|
||||
products.name ILIKE ? OR
|
||||
sales_users.name ILIKE ? OR
|
||||
CONCAT(
|
||||
marketings.so_number,
|
||||
'-',
|
||||
COALESCE(TO_CHAR(marketing_delivery_products.delivery_date, 'YYYYMMDD'), ''),
|
||||
'-',
|
||||
COALESCE(product_warehouses.warehouse_id::text, '')
|
||||
) ILIKE ?
|
||||
)`,
|
||||
searchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern)
|
||||
}
|
||||
|
||||
if filters.CustomerId > 0 {
|
||||
@@ -139,6 +190,41 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
||||
db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId)
|
||||
}
|
||||
|
||||
if filters.AreaId > 0 || filters.LocationId > 0 {
|
||||
db = db.Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
|
||||
Joins("LEFT JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id")
|
||||
|
||||
if filters.AreaId > 0 {
|
||||
db = db.Where("project_flocks.area_id = ?", filters.AreaId)
|
||||
}
|
||||
|
||||
if filters.LocationId > 0 {
|
||||
db = db.Where("project_flocks.location_id = ?", filters.LocationId)
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
})
|
||||
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 != "" {
|
||||
|
||||
@@ -249,7 +249,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
|
||||
|
||||
// Hitung total_weight dan total_price otomatis
|
||||
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
|
||||
totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty
|
||||
totalPrice := requestedProduct.UnitPrice * totalWeight
|
||||
|
||||
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
||||
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
||||
@@ -363,7 +363,7 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
|
||||
|
||||
// Hitung total_weight dan total_price otomatis
|
||||
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
|
||||
totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty
|
||||
totalPrice := requestedProduct.UnitPrice * totalWeight
|
||||
|
||||
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
||||
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
||||
|
||||
@@ -294,7 +294,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
||||
|
||||
// Hitung total_weight dan total_price otomatis
|
||||
totalWeight := rp.Qty * rp.AvgWeight
|
||||
totalPrice := rp.UnitPrice * rp.Qty
|
||||
totalPrice := rp.UnitPrice * totalWeight
|
||||
|
||||
updateBody := map[string]any{
|
||||
"product_warehouse_id": rp.ProductWarehouseId,
|
||||
@@ -594,7 +594,7 @@ func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Cont
|
||||
|
||||
// Hitung total_weight dan total_price otomatis
|
||||
totalWeight := rp.Qty * rp.AvgWeight
|
||||
totalPrice := rp.UnitPrice * rp.Qty
|
||||
totalPrice := rp.UnitPrice * totalWeight
|
||||
|
||||
marketingProduct := &entity.MarketingProduct{
|
||||
MarketingId: marketingId,
|
||||
|
||||
@@ -52,7 +52,7 @@ func (s areaService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Ar
|
||||
areas, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
if params.Search != "" {
|
||||
return db.Where("name LIKE ?", "%"+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("name LIKE ?", "%"+params.Search+"%")
|
||||
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
|
||||
@@ -15,9 +15,9 @@ func ConfigChecklistRoutes(v1 fiber.Router, u user.UserService, s configChecklis
|
||||
route := v1.Group("/config-checklists")
|
||||
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_DailyChecklistActivityConfig), ctrl.GetAll)
|
||||
route.Post("/", m.RequirePermissions(m.P_DailyChecklistActivityConfig), ctrl.CreateOne)
|
||||
route.Get("/:id", m.RequirePermissions(m.P_DailyChecklistActivityConfig), ctrl.GetOne)
|
||||
route.Patch("/:id", m.RequirePermissions(m.P_DailyChecklistActivityConfig), ctrl.UpdateOne)
|
||||
route.Delete("/:id", m.RequirePermissions(m.P_DailyChecklistActivityConfig), ctrl.DeleteOne)
|
||||
}
|
||||
|
||||
@@ -76,6 +76,9 @@ func (s *configChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.PercentageThresholdBad > req.PercentageThresholdEnough {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "percentage_threshold_bad cannot be greater than percentage_threshold_enough")
|
||||
}
|
||||
|
||||
date, err := time.Parse("2006-01-02", req.Date)
|
||||
if err != nil {
|
||||
@@ -100,6 +103,11 @@ func (s configChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update,
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.PercentageThresholdBad != nil && req.PercentageThresholdEnough != nil {
|
||||
if *req.PercentageThresholdBad > *req.PercentageThresholdEnough {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "percentage_threshold_bad cannot be greater than percentage_threshold_enough")
|
||||
}
|
||||
}
|
||||
|
||||
updateBody := make(map[string]any)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ type CustomerRelationDTO struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
AccountNumber string `json:"account_number"`
|
||||
Address string `json:"address,omitempty"`
|
||||
Balance float64 `json:"balance"`
|
||||
Pic *userDTO.UserRelationDTO `json:"pic,omitempty"`
|
||||
}
|
||||
@@ -52,6 +53,8 @@ func ToCustomerRelationDTO(e entity.Customer) CustomerRelationDTO {
|
||||
Name: e.Name,
|
||||
Type: e.Type,
|
||||
AccountNumber: e.AccountNumber,
|
||||
Address: e.Address,
|
||||
Balance: e.Balance,
|
||||
Pic: pic,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,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("name LIKE ?", "%"+params.Search+"%")
|
||||
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
|
||||
@@ -15,9 +15,9 @@ func EmployeesRoutes(v1 fiber.Router, u user.UserService, s employees.EmployeesS
|
||||
route := v1.Group("/employees")
|
||||
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_DailyChecklistEmployee), ctrl.GetAll)
|
||||
route.Post("/", m.RequirePermissions(m.P_DailyChecklistEmployee), ctrl.CreateOne)
|
||||
route.Get("/:id", m.RequirePermissions(m.P_DailyChecklistEmployee), ctrl.GetOne)
|
||||
route.Patch("/:id", m.RequirePermissions(m.P_DailyChecklistEmployee), ctrl.UpdateOne)
|
||||
route.Delete("/:id", m.RequirePermissions(m.P_DailyChecklistEmployee), ctrl.DeleteOne)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
||||
employeess, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
if params.Search != "" {
|
||||
db = db.Where("employees.name LIKE ?", "%"+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("name LIKE ?", "%"+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("name LIKE ?", "%"+params.Search+"%")
|
||||
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
|
||||
@@ -54,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("name LIKE ?", "%"+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("name LIKE ?", "%"+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,13 @@ type NonstockDetailDTO struct {
|
||||
NonstockListDTO
|
||||
}
|
||||
|
||||
type NonstockSupplierDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Alias string `json:"alias"`
|
||||
Category string `json:"category"`
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToNonstockRelationDTO(e entity.Nonstock) NonstockRelationDTO {
|
||||
@@ -99,21 +105,26 @@ 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,
|
||||
})
|
||||
}
|
||||
|
||||
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{}).
|
||||
@@ -66,13 +66,16 @@ func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm
|
||||
existingMap[rel.SupplierId] = struct{}{}
|
||||
}
|
||||
|
||||
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 _, exists := existingMap[rel.SupplierId]; exists {
|
||||
continue
|
||||
}
|
||||
record := entity.NonstockSupplier{NonstockId: nonstockID, SupplierId: id}
|
||||
record := entity.NonstockSupplier{
|
||||
NonstockId: nonstockID,
|
||||
SupplierId: rel.SupplierId,
|
||||
}
|
||||
if err := db.WithContext(ctx).Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ func (s nonstockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
|
||||
db = s.withRelations(db)
|
||||
|
||||
if params.Search != "" {
|
||||
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
||||
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
@@ -111,8 +111,24 @@ 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.SupplierIDs) > 0 {
|
||||
seen := make(map[uint]struct{}, len(req.SupplierIDs))
|
||||
supplierLinks = make([]entity.NonstockSupplier, 0, len(req.SupplierIDs))
|
||||
supplierIDs = make([]uint, 0, len(req.SupplierIDs))
|
||||
for _, supplierID := range req.SupplierIDs {
|
||||
if _, exists := seen[supplierID]; exists {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplierID))
|
||||
}
|
||||
seen[supplierID] = struct{}{}
|
||||
supplierIDs = append(supplierIDs, supplierID)
|
||||
supplierLinks = append(supplierLinks, entity.NonstockSupplier{
|
||||
SupplierId: supplierID,
|
||||
})
|
||||
}
|
||||
supplierList, supplierErr := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
|
||||
if supplierErr != nil {
|
||||
s.Log.Errorf("Failed to validate suppliers: %+v", supplierErr)
|
||||
@@ -155,7 +171,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 +209,26 @@ 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 {
|
||||
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.SupplierIDs) > 0 {
|
||||
seen := make(map[uint]struct{}, len(*req.SupplierIDs))
|
||||
supplierLinks = make([]entity.NonstockSupplier, 0, len(*req.SupplierIDs))
|
||||
supplierIDs := make([]uint, 0, len(*req.SupplierIDs))
|
||||
for _, supplierID := range *req.SupplierIDs {
|
||||
if _, exists := seen[supplierID]; exists {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplierID))
|
||||
}
|
||||
seen[supplierID] = struct{}{}
|
||||
supplierIDs = append(supplierIDs, supplierID)
|
||||
supplierLinks = append(supplierLinks, entity.NonstockSupplier{
|
||||
SupplierId: supplierID,
|
||||
})
|
||||
}
|
||||
|
||||
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 +280,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,17 +1,17 @@
|
||||
package validation
|
||||
|
||||
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"`
|
||||
Flags []string `json:"flags" validate:"dive,max=50"`
|
||||
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,omitempty" validate:"omitempty,dive,gt=0"`
|
||||
Flags []string `json:"flags" validate:"dive,max=50"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=50"`
|
||||
UomID *uint `json:"uom_id,omitempty" validate:"omitempty,gt=0"`
|
||||
SupplierIDs *[]uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
|
||||
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive,max=50"`
|
||||
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"`
|
||||
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive,max=50"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
|
||||
@@ -15,9 +15,9 @@ func PhaseActivityRoutes(v1 fiber.Router, u user.UserService, s phaseActivity.Ph
|
||||
route := v1.Group("/phase-activities")
|
||||
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_DailyChecklistActivity), ctrl.GetAll)
|
||||
route.Post("/", m.RequirePermissions(m.P_DailyChecklistActivity), ctrl.CreateOne)
|
||||
route.Get("/:id", m.RequirePermissions(m.P_DailyChecklistActivity), ctrl.GetOne)
|
||||
route.Patch("/:id", m.RequirePermissions(m.P_DailyChecklistActivity), ctrl.UpdateOne)
|
||||
route.Delete("/:id", m.RequirePermissions(m.P_DailyChecklistActivity), ctrl.DeleteOne)
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func (s phaseActivityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]
|
||||
phaseActivitys, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
if params.Search != "" {
|
||||
db = db.Where("name LIKE ?", "%"+params.Search+"%")
|
||||
db = db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
if params.PhaseIDs != "" {
|
||||
ids := parseIDs(params.PhaseIDs)
|
||||
@@ -110,6 +110,17 @@ func (s *phaseActivityService) CreateOne(c *fiber.Ctx, req *validation.Create) (
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "time_type cannot be empty")
|
||||
}
|
||||
|
||||
existing, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("phase_id = ? AND name = ? AND time_type = ?", phase.Id, name, timeType)
|
||||
})
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Failed to check phaseActivity uniqueness: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
if existing != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "phase activity with same name and time_type already exists")
|
||||
}
|
||||
|
||||
createBody := &entity.PhaseActivity{
|
||||
PhaseId: phase.Id,
|
||||
Name: name,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ func PhasesRoutes(v1 fiber.Router, u user.UserService, s phases.PhasesService) {
|
||||
route := v1.Group("/phases")
|
||||
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_DailyChecklistActivity), ctrl.GetAll)
|
||||
route.Post("/", m.RequirePermissions(m.P_DailyChecklistActivity), ctrl.CreateOne)
|
||||
route.Get("/:id", m.RequirePermissions(m.P_DailyChecklistActivity), ctrl.GetOne)
|
||||
route.Patch("/:id", m.RequirePermissions(m.P_DailyChecklistActivity), ctrl.UpdateOne)
|
||||
route.Delete("/:id", m.RequirePermissions(m.P_DailyChecklistActivity), ctrl.DeleteOne)
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func (s phasesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.
|
||||
phasess, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
if params.Search != "" {
|
||||
return db.Where("name LIKE ?", "%"+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("name LIKE ?", "%"+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("name LIKE ?", "%"+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
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user