mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-23 23:05:44 +00:00
Merge remote-tracking branch 'origin/development' into staging
This commit is contained in:
@@ -16,6 +16,7 @@ require (
|
|||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jackc/pgconn v1.14.1
|
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/redis/go-redis/v9 v9.14.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/spf13/viper v1.19.0
|
github.com/spf13/viper v1.19.0
|
||||||
@@ -60,7 +61,6 @@ require (
|
|||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
|
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // 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/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // 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/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 h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
|
||||||
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
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 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
|
||||||
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
|
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 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
|
||||||
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
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=
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
|||||||
@@ -228,7 +228,13 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case delta > 0:
|
case delta > 0:
|
||||||
allocationRes, err := s.allocateFromStock(ctx, tx, productWarehouseID, req.UsableKey, req.UsableID, delta)
|
|
||||||
|
var excludedStockables []fifo.StockableKey
|
||||||
|
if cfg.ExcludedStockables != nil {
|
||||||
|
excludedStockables = cfg.ExcludedStockables
|
||||||
|
}
|
||||||
|
|
||||||
|
allocationRes, err := s.allocateFromStock(ctx, tx, productWarehouseID, req.UsableKey, req.UsableID, delta, excludedStockables)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -410,8 +416,9 @@ func (s *fifoService) allocateFromStock(
|
|||||||
usableKey fifo.UsableKey,
|
usableKey fifo.UsableKey,
|
||||||
usableID uint,
|
usableID uint,
|
||||||
requestQty float64,
|
requestQty float64,
|
||||||
|
excludedStockables []fifo.StockableKey,
|
||||||
) (*allocationOutcome, error) {
|
) (*allocationOutcome, error) {
|
||||||
lots, err := s.fetchStockLots(ctx, tx, productWarehouseID)
|
lots, err := s.fetchStockLots(ctx, tx, productWarehouseID, excludedStockables)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -492,14 +499,24 @@ func (s *fifoService) allocateFromStock(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *fifoService) fetchStockLots(ctx context.Context, tx *gorm.DB, productWarehouseID uint) ([]stockLot, error) {
|
func (s *fifoService) fetchStockLots(ctx context.Context, tx *gorm.DB, productWarehouseID uint, excludedStockables []fifo.StockableKey) ([]stockLot, error) {
|
||||||
configs := fifo.Stockables()
|
configs := fifo.Stockables()
|
||||||
if len(configs) == 0 {
|
if len(configs) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create exclusion set for faster lookup
|
||||||
|
excludedSet := make(map[fifo.StockableKey]bool)
|
||||||
|
for _, key := range excludedStockables {
|
||||||
|
excludedSet[key] = true
|
||||||
|
}
|
||||||
|
|
||||||
var lots []stockLot
|
var lots []stockLot
|
||||||
for key, cfg := range configs {
|
for key, cfg := range configs {
|
||||||
|
// Skip excluded stockables
|
||||||
|
if excludedSet[key] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
usesNumericTime := cfg.Columns.CreatedAt == cfg.Columns.ID
|
usesNumericTime := cfg.Columns.CreatedAt == cfg.Columns.ID
|
||||||
|
|
||||||
@@ -616,7 +633,13 @@ func (s *fifoService) resolvePendingForWarehouse(ctx context.Context, tx *gorm.D
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
outcome, err := s.allocateFromStock(ctx, tx, productWarehouseID, candidate.UsableKey, candidate.UsableID, candidate.Pending)
|
// Get excluded stockables from candidate usable config
|
||||||
|
var excludedStockables []fifo.StockableKey
|
||||||
|
if candidate.Config.ExcludedStockables != nil {
|
||||||
|
excludedStockables = candidate.Config.ExcludedStockables
|
||||||
|
}
|
||||||
|
|
||||||
|
outcome, err := s.allocateFromStock(ctx, tx, productWarehouseID, candidate.UsableKey, candidate.UsableID, candidate.Pending, excludedStockables)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ func FiberConfig() fiber.Config {
|
|||||||
CaseSensitive: true,
|
CaseSensitive: true,
|
||||||
ServerHeader: "Fiber",
|
ServerHeader: "Fiber",
|
||||||
AppName: "Fiber API",
|
AppName: "Fiber API",
|
||||||
|
BodyLimit: 8 * 1024 * 1024,
|
||||||
ErrorHandler: utils.ErrorHandler,
|
ErrorHandler: utils.ErrorHandler,
|
||||||
JSONEncoder: sonic.Marshal,
|
JSONEncoder: sonic.Marshal,
|
||||||
JSONDecoder: sonic.Unmarshal,
|
JSONDecoder: sonic.Unmarshal,
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
DROP TABLE IF EXISTS expenses;
|
DROP SEQUENCE IF EXISTS expenses_ref_seq;
|
||||||
|
DROP TABLE IF EXISTS expenses;
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
-- Drop function and sequence for sales order numbers
|
-- Drop function and sequence for sales order numbers
|
||||||
DROP FUNCTION IF EXISTS generate_so_number();
|
|
||||||
DROP SEQUENCE IF EXISTS so_number_seq;
|
DROP SEQUENCE IF EXISTS so_number_seq;
|
||||||
|
DROP FUNCTION IF EXISTS generate_so_number();
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
DROP TABLE IF EXISTS daily_checklist_tasks;
|
-- Drop tables in correct order (child tables before parent tables)
|
||||||
|
DROP TABLE IF EXISTS daily_checklist_activity_task_assignments; -- Child table with FK to daily_checklist_activity_tasks
|
||||||
DROP TABLE IF EXISTS daily_checklist_activity_task_assignees;
|
DROP TABLE IF EXISTS daily_checklist_activity_task_assignees;
|
||||||
DROP TABLE IF EXISTS daily_checklist_activity_tasks;
|
DROP TABLE IF EXISTS daily_checklist_activity_tasks;
|
||||||
|
DROP TABLE IF EXISTS daily_checklist_tasks;
|
||||||
DROP TABLE IF EXISTS daily_checklist_phases;
|
DROP TABLE IF EXISTS daily_checklist_phases;
|
||||||
DROP TABLE IF EXISTS daily_checklists;
|
DROP TABLE IF EXISTS daily_checklists;
|
||||||
DROP TABLE IF EXISTS checklists;
|
DROP TABLE IF EXISTS checklists;
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- Revert back to NO ACTION (RESTRICT behavior)
|
||||||
|
ALTER TABLE expense_nonstocks DROP CONSTRAINT IF EXISTS fk_expense_nonstocks_expense_id;
|
||||||
|
|
||||||
|
ALTER TABLE expense_nonstocks
|
||||||
|
ADD CONSTRAINT fk_expense_nonstocks_expense_id
|
||||||
|
FOREIGN KEY (expense_id) REFERENCES expenses(id)
|
||||||
|
ON DELETE NO ACTION;
|
||||||
|
|
||||||
|
-- Revert expense_realizations FK
|
||||||
|
ALTER TABLE expense_realizations DROP CONSTRAINT IF EXISTS fk_expense_realizations_nonstock_id;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
ADD CONSTRAINT fk_expense_realizations_nonstock_id
|
||||||
|
FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id)
|
||||||
|
ON DELETE NO ACTION;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- Drop existing FK constraints
|
||||||
|
ALTER TABLE expense_nonstocks DROP CONSTRAINT IF EXISTS fk_expense_nonstocks_expense_id;
|
||||||
|
|
||||||
|
-- Recreate with ON DELETE CASCADE
|
||||||
|
ALTER TABLE expense_nonstocks
|
||||||
|
ADD CONSTRAINT fk_expense_nonstocks_expense_id
|
||||||
|
FOREIGN KEY (expense_id) REFERENCES expenses(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- Drop and recreate expense_realizations FK
|
||||||
|
ALTER TABLE expense_realizations DROP CONSTRAINT IF EXISTS fk_expense_realizations_nonstock_id;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
ADD CONSTRAINT fk_expense_realizations_nonstock_id
|
||||||
|
FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- Revert back to NO ACTION (for rollback safety)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE marketing_products DROP CONSTRAINT IF EXISTS fk_marketing_products_marketing_id;
|
||||||
|
|
||||||
|
ALTER TABLE marketing_products
|
||||||
|
ADD CONSTRAINT fk_marketing_products_marketing_id
|
||||||
|
FOREIGN KEY (marketing_id) REFERENCES marketings(id)
|
||||||
|
ON DELETE NO ACTION;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE marketing_delivery_products DROP CONSTRAINT IF EXISTS fk_marketing_delivery_products_marketing_product_id;
|
||||||
|
|
||||||
|
ALTER TABLE marketing_delivery_products
|
||||||
|
ADD CONSTRAINT fk_marketing_delivery_products_marketing_product_id
|
||||||
|
FOREIGN KEY (marketing_product_id) REFERENCES marketing_products(id)
|
||||||
|
ON DELETE NO ACTION;
|
||||||
|
END $$;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
-- Ensure marketing_products FK is CASCADE (it should already be, but let's make sure)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
-- Drop existing FK if exists
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_marketing_products_marketing_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE marketing_products DROP CONSTRAINT fk_marketing_products_marketing_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Recreate with ON DELETE CASCADE
|
||||||
|
ALTER TABLE marketing_products
|
||||||
|
ADD CONSTRAINT fk_marketing_products_marketing_id
|
||||||
|
FOREIGN KEY (marketing_id) REFERENCES marketings(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Ensure marketing_delivery_products FK is CASCADE
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
-- Drop existing FK if exists
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_marketing_delivery_products_marketing_product_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE marketing_delivery_products DROP CONSTRAINT fk_marketing_delivery_products_marketing_product_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Recreate with ON DELETE CASCADE
|
||||||
|
ALTER TABLE marketing_delivery_products
|
||||||
|
ADD CONSTRAINT fk_marketing_delivery_products_marketing_product_id
|
||||||
|
FOREIGN KEY (marketing_product_id) REFERENCES marketing_products(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
END $$;
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
-- Drop foreign key and column
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
DROP CONSTRAINT IF EXISTS fk_laying_transfers_product_warehouse_id;
|
||||||
|
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
DROP COLUMN IF EXISTS product_warehouse_id;
|
||||||
|
|
||||||
|
-- Drop index
|
||||||
|
DROP INDEX IF EXISTS idx_laying_transfers_product_warehouse_id;
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
-- Add product_warehouse_id to laying_transfers for FIFO support
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD COLUMN product_warehouse_id BIGINT;
|
||||||
|
|
||||||
|
-- Add foreign key
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||||
|
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;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Add index
|
||||||
|
CREATE INDEX idx_laying_transfers_product_warehouse_id
|
||||||
|
ON laying_transfers(product_warehouse_id);
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
-- Rollback: Remove STOCKABLE fields from laying_transfers
|
||||||
|
|
||||||
|
-- Drop index
|
||||||
|
DROP INDEX IF EXISTS idx_laying_transfers_dest_product_warehouse_id;
|
||||||
|
|
||||||
|
-- Drop foreign key constraint
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
DROP CONSTRAINT IF EXISTS fk_laying_transfers_dest_product_warehouse_id;
|
||||||
|
|
||||||
|
-- Drop columns
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
DROP COLUMN IF EXISTS dest_product_warehouse_id,
|
||||||
|
DROP COLUMN IF EXISTS total_qty,
|
||||||
|
DROP COLUMN IF EXISTS total_used;
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
-- Add STOCKABLE fields to laying_transfers for destination warehouse
|
||||||
|
-- This enables Transfer to Laying to work as DUAL ROLE (Stockable + Usable)
|
||||||
|
|
||||||
|
-- Add columns 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,
|
||||||
|
ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0;
|
||||||
|
|
||||||
|
-- Add foreign key constraint
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||||
|
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 $$;
|
||||||
|
|
||||||
|
-- Add index for performance
|
||||||
|
CREATE INDEX idx_laying_transfers_dest_product_warehouse_id
|
||||||
|
ON laying_transfers(dest_product_warehouse_id);
|
||||||
|
|
||||||
|
-- Add comment 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 STOCKABLE role';
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- Remove chart_data, uniform_date, and related indexes
|
||||||
|
DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_uniform_date;
|
||||||
|
DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_unique;
|
||||||
|
|
||||||
|
ALTER TABLE project_flock_kandang_uniformity
|
||||||
|
DROP COLUMN IF EXISTS chart_data,
|
||||||
|
DROP COLUMN IF EXISTS uniform_date;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
-- Add uniform_date (if missing), chart_data, and unique constraint for uniformity records
|
||||||
|
ALTER TABLE project_flock_kandang_uniformity
|
||||||
|
ADD COLUMN IF NOT EXISTS uniform_date TIMESTAMPTZ,
|
||||||
|
ADD COLUMN IF NOT EXISTS chart_data JSONB;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'project_flock_kandang_uniformity'
|
||||||
|
AND column_name = 'deleted_at'
|
||||||
|
) THEN
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_unique
|
||||||
|
ON project_flock_kandang_uniformity (project_flock_kandang_id, week, uniform_date)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
ELSE
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_unique
|
||||||
|
ON project_flock_kandang_uniformity (project_flock_kandang_id, week, uniform_date);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_uniform_date
|
||||||
|
ON project_flock_kandang_uniformity (uniform_date);
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
-- Remove expense_nonstock_id from stock_transfer_details
|
||||||
|
ALTER TABLE stock_transfer_details DROP CONSTRAINT IF EXISTS fk_stock_transfer_details_expense_nonstock;
|
||||||
|
ALTER TABLE stock_transfer_details DROP COLUMN IF EXISTS expense_nonstock_id;
|
||||||
|
DROP INDEX IF EXISTS idx_stock_transfer_details_expense_nonstock_id;
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
-- Add expense_nonstock_id to stock_transfer_details
|
||||||
|
-- This allows tracking expedition/transport costs for stock transfers (same as purchase)
|
||||||
|
|
||||||
|
ALTER TABLE stock_transfer_details
|
||||||
|
ADD COLUMN expense_nonstock_id BIGINT,
|
||||||
|
ADD CONSTRAINT fk_stock_transfer_details_expense_nonstock
|
||||||
|
FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- Create index for better query performance
|
||||||
|
CREATE INDEX idx_stock_transfer_details_expense_nonstock_id ON stock_transfer_details(expense_nonstock_id);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS config_checklists;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS config_checklists (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
date DATE NOT NULL,
|
||||||
|
percentage_threshold_bad INTEGER NOT NULL,
|
||||||
|
percentage_threshold_enough INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE payments
|
||||||
|
DROP COLUMN IF EXISTS party_account_number;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE payments
|
||||||
|
ADD COLUMN IF NOT EXISTS party_account_number VARCHAR(50);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS projects;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS projects;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- Revert master data foreign keys to CASCADE delete (except FCR)
|
||||||
|
ALTER TABLE nonstock_suppliers
|
||||||
|
DROP CONSTRAINT IF EXISTS nonstock_suppliers_nonstock_id_fkey,
|
||||||
|
DROP CONSTRAINT IF EXISTS nonstock_suppliers_supplier_id_fkey;
|
||||||
|
|
||||||
|
ALTER TABLE nonstock_suppliers
|
||||||
|
ADD CONSTRAINT nonstock_suppliers_nonstock_id_fkey FOREIGN KEY (nonstock_id)
|
||||||
|
REFERENCES nonstocks (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
ADD CONSTRAINT nonstock_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
|
||||||
|
REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE product_suppliers
|
||||||
|
DROP CONSTRAINT IF EXISTS product_suppliers_product_id_fkey,
|
||||||
|
DROP CONSTRAINT IF EXISTS product_suppliers_supplier_id_fkey;
|
||||||
|
|
||||||
|
ALTER TABLE product_suppliers
|
||||||
|
ADD CONSTRAINT product_suppliers_product_id_fkey FOREIGN KEY (product_id)
|
||||||
|
REFERENCES products (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
ADD CONSTRAINT product_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
|
||||||
|
REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- Update master data foreign keys to RESTRICT delete (except FCR)
|
||||||
|
ALTER TABLE nonstock_suppliers
|
||||||
|
DROP CONSTRAINT IF EXISTS nonstock_suppliers_nonstock_id_fkey,
|
||||||
|
DROP CONSTRAINT IF EXISTS nonstock_suppliers_supplier_id_fkey;
|
||||||
|
|
||||||
|
ALTER TABLE nonstock_suppliers
|
||||||
|
ADD CONSTRAINT nonstock_suppliers_nonstock_id_fkey FOREIGN KEY (nonstock_id)
|
||||||
|
REFERENCES nonstocks (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
ADD CONSTRAINT nonstock_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
|
||||||
|
REFERENCES suppliers (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE product_suppliers
|
||||||
|
DROP CONSTRAINT IF EXISTS product_suppliers_product_id_fkey,
|
||||||
|
DROP CONSTRAINT IF EXISTS product_suppliers_supplier_id_fkey;
|
||||||
|
|
||||||
|
ALTER TABLE product_suppliers
|
||||||
|
ADD CONSTRAINT product_suppliers_product_id_fkey FOREIGN KEY (product_id)
|
||||||
|
REFERENCES products (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
ADD CONSTRAINT product_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
|
||||||
|
REFERENCES suppliers (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
+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,59 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v4;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'recordings' AND column_name = 'hen_day'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE recordings RENAME COLUMN hen_day TO hand_day;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'recordings' AND column_name = 'hen_house'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE recordings RENAME COLUMN hen_house TO hand_house;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'recordings' AND column_name = 'egg_mass'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE recordings RENAME COLUMN egg_mass TO egg_mesh;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
ADD COLUMN IF NOT EXISTS daily_gain NUMERIC(7,3),
|
||||||
|
ADD COLUMN IF NOT EXISTS avg_daily_gain NUMERIC(7,3);
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
ADD CONSTRAINT chk_recordings_nonnegatives_v3 CHECK (
|
||||||
|
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
|
||||||
|
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND
|
||||||
|
(daily_gain IS NULL OR daily_gain >= 0) AND
|
||||||
|
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
|
||||||
|
(cum_intake IS NULL OR cum_intake >= 0) AND
|
||||||
|
(fcr_value IS NULL OR fcr_value >= 0) AND
|
||||||
|
(total_chick_qty IS NULL OR total_chick_qty >= 0) AND
|
||||||
|
(hand_day IS NULL OR hand_day >= 0) AND
|
||||||
|
(hand_house IS NULL OR hand_house >= 0) AND
|
||||||
|
(feed_intake IS NULL OR feed_intake >= 0) AND
|
||||||
|
(egg_mesh IS NULL OR egg_mesh >= 0) AND
|
||||||
|
(egg_weight IS NULL OR egg_weight >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v3;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
DROP COLUMN IF EXISTS daily_gain,
|
||||||
|
DROP COLUMN IF EXISTS avg_daily_gain;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'recordings' AND column_name = 'hand_day'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE recordings RENAME COLUMN hand_day TO hen_day;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'recordings' AND column_name = 'hand_house'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE recordings RENAME COLUMN hand_house TO hen_house;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'recordings' AND column_name = 'egg_mesh'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE recordings RENAME COLUMN egg_mesh TO egg_mass;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
ADD CONSTRAINT chk_recordings_nonnegatives_v4 CHECK (
|
||||||
|
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
|
||||||
|
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND
|
||||||
|
(cum_intake IS NULL OR cum_intake >= 0) AND
|
||||||
|
(fcr_value IS NULL OR fcr_value >= 0) AND
|
||||||
|
(total_chick_qty IS NULL OR total_chick_qty >= 0) AND
|
||||||
|
(hen_day IS NULL OR hen_day >= 0) AND
|
||||||
|
(hen_house IS NULL OR hen_house >= 0) AND
|
||||||
|
(feed_intake IS NULL OR feed_intake >= 0) AND
|
||||||
|
(egg_mass IS NULL OR egg_mass >= 0) AND
|
||||||
|
(egg_weight IS NULL OR egg_weight >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -299,6 +299,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
|||||||
Tax: tax,
|
Tax: tax,
|
||||||
ExpiryPeriod: seed.Expiry,
|
ExpiryPeriod: seed.Expiry,
|
||||||
CreatedBy: createdBy,
|
CreatedBy: createdBy,
|
||||||
|
IsVisible: seed.IsVisible,
|
||||||
}
|
}
|
||||||
if err := tx.Create(&product).Error; err != nil {
|
if err := tx.Create(&product).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigChecklist struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
Date time.Time `gorm:"type:date;not null"`
|
||||||
|
PercentageThresholdBad int `gorm:"not null"`
|
||||||
|
PercentageThresholdEnough int `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -5,15 +5,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ExpenseNonstock struct {
|
type ExpenseNonstock struct {
|
||||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
ExpenseId *uint64 `gorm:""`
|
ExpenseId *uint64 `gorm:""`
|
||||||
ProjectFlockKandangId *uint64 `gorm:""`
|
ProjectFlockKandangId *uint64 `gorm:""`
|
||||||
KandangId *uint64 `gorm:""`
|
KandangId *uint64 `gorm:""`
|
||||||
NonstockId *uint64 `gorm:""`
|
NonstockId *uint64 `gorm:""`
|
||||||
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
Price float64 `gorm:"type:numeric(15,3);not null;column:price"`
|
Price float64 `gorm:"type:numeric(15,3);not null;column:price"`
|
||||||
Notes string `gorm:"type:text;column:notes"`
|
Notes string `gorm:"type:text;column:notes"`
|
||||||
CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"`
|
CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"`
|
||||||
|
|
||||||
Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"`
|
Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"`
|
||||||
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
|
|||||||
@@ -12,18 +12,16 @@ type LayingTransfer struct {
|
|||||||
FromProjectFlockId uint `gorm:"not null"`
|
FromProjectFlockId uint `gorm:"not null"`
|
||||||
ToProjectFlockId uint `gorm:"not null"`
|
ToProjectFlockId uint `gorm:"not null"`
|
||||||
TransferDate time.Time `gorm:"type:date;not null"`
|
TransferDate time.Time `gorm:"type:date;not null"`
|
||||||
PendingUsageQty *float64 `gorm:"type:numeric(15,3)"`
|
|
||||||
UsageQty *float64 `gorm:"type:numeric(15,3)"`
|
|
||||||
Notes string `gorm:"type:text"`
|
Notes string `gorm:"type:text"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
|
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
|
||||||
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
|
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
||||||
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
||||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ type LayingTransferSource struct {
|
|||||||
LayingTransferId uint `gorm:"index;not null"`
|
LayingTransferId uint `gorm:"index;not null"`
|
||||||
SourceProjectFlockKandangId uint `gorm:"not null"`
|
SourceProjectFlockKandangId uint `gorm:"not null"`
|
||||||
ProductWarehouseId *uint `gorm:""`
|
ProductWarehouseId *uint `gorm:""`
|
||||||
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
|
||||||
|
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
|
||||||
Note string `gorm:"type:text"`
|
Note string `gorm:"type:text"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ type LayingTransferTarget struct {
|
|||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
LayingTransferId uint `gorm:"index;not null"`
|
LayingTransferId uint `gorm:"index;not null"`
|
||||||
TargetProjectFlockKandangId uint `gorm:"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:""`
|
ProductWarehouseId *uint `gorm:""`
|
||||||
Note string `gorm:"type:text"`
|
Note string `gorm:"type:text"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|||||||
@@ -7,22 +7,23 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Payment struct {
|
type Payment struct {
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||||
PaymentCode string `gorm:"type:varchar(50);not null"`
|
PaymentCode string `gorm:"type:varchar(50);not null"`
|
||||||
ReferenceNumber *string `gorm:"type:varchar(100)"`
|
ReferenceNumber *string `gorm:"type:varchar(100)"`
|
||||||
TransactionType string `gorm:"type:varchar(50)"`
|
TransactionType string `gorm:"type:varchar(50)"`
|
||||||
PartyType string `gorm:"type:varchar(50);not null;index:payments_party_polymorphic,priority:1"`
|
PartyType string `gorm:"type:varchar(50);not null;index:payments_party_polymorphic,priority:1"`
|
||||||
PartyId uint `gorm:"not null;index:payments_party_polymorphic,priority:2"`
|
PartyId uint `gorm:"not null;index:payments_party_polymorphic,priority:2"`
|
||||||
PaymentDate time.Time `gorm:"not null"`
|
PartyAccountNumber *string `gorm:"type:varchar(50)"`
|
||||||
PaymentMethod string `gorm:"type:varchar(20);not null"`
|
PaymentDate time.Time `gorm:"not null"`
|
||||||
BankId *uint `gorm:"not null;index:idx_payments_bank_id"`
|
PaymentMethod string `gorm:"type:varchar(20);not null"`
|
||||||
Direction string `gorm:"type:varchar(5);not null"`
|
BankId *uint `gorm:"not null;index:idx_payments_bank_id"`
|
||||||
Nominal float64 `gorm:"type:numeric(15,3);not null"`
|
Direction string `gorm:"type:varchar(5);not null"`
|
||||||
Notes string `gorm:"type:text;not null"`
|
Nominal float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
Notes string `gorm:"type:text;not null"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
CreatedBy uint `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
CreatedBy uint `gorm:"index" json:"-"`
|
||||||
|
|
||||||
BankWarehouse Bank `gorm:"foreignKey:BankId;references:Id"`
|
BankWarehouse Bank `gorm:"foreignKey:BankId;references:Id"`
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Phases struct {
|
type Phases struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null"`
|
Name string `gorm:"not null"`
|
||||||
IsActive bool `gorm:"not null;default:true"`
|
IsActive bool `gorm:"not null;default:true"`
|
||||||
Category string `gorm:"type:category_code;not null"`
|
Category string `gorm:"type:category_code;not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
ActivityCount int `gorm:"-" json:"-"`
|
||||||
|
|
||||||
Activities []PhaseActivity `gorm:"foreignKey:PhaseId;references:Id"`
|
Activities []PhaseActivity `gorm:"foreignKey:PhaseId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ type Product struct {
|
|||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
IsVisible bool `gorm:"column:is_visible;default:true"`
|
IsVisible bool ``
|
||||||
|
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
|
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type ProjectFlockKandangUniformity struct {
|
type ProjectFlockKandangUniformity struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Uniformity float64 `gorm:"type:numeric(15,3)"`
|
Uniformity float64 `gorm:"type:numeric(15,3)"`
|
||||||
Week int `gorm:"not null"`
|
Week int `gorm:"not null"`
|
||||||
Cv float64 `gorm:"type:numeric(15,3)"`
|
Cv float64 `gorm:"type:numeric(15,3)"`
|
||||||
ChickQtyOfWeight float64 `gorm:"type:numeric(15,3)"`
|
ChickQtyOfWeight float64 `gorm:"type:numeric(15,3)"`
|
||||||
MeanUp float64 `gorm:"type:numeric(15,3)"`
|
MeanUp float64 `gorm:"type:numeric(15,3)"`
|
||||||
MeanDown float64 `gorm:"type:numeric(15,3)"`
|
MeanDown float64 `gorm:"type:numeric(15,3)"`
|
||||||
ProjectFlockKandangId uint `gorm:"not null"`
|
ProjectFlockKandangId uint `gorm:"not null"`
|
||||||
UniformQty float64 `gorm:"type:numeric(15,3)"`
|
UniformQty float64 `gorm:"type:numeric(15,3)"`
|
||||||
NotUniformQty float64 `gorm:"type:numeric(15,3)"`
|
NotUniformQty float64 `gorm:"type:numeric(15,3)"`
|
||||||
UniformDate *time.Time `gorm:"type:timestamptz"`
|
ChartData json.RawMessage `gorm:"type:jsonb"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
UniformDate *time.Time `gorm:"type:timestamptz"`
|
||||||
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
|
||||||
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ type ProjectFlockKandang struct {
|
|||||||
ClosedAt *time.Time `gorm:"index"`
|
ClosedAt *time.Time `gorm:"index"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|
||||||
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||||
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
LatestProjectFlockApproval *Approval `gorm:"-" json:"-"`
|
||||||
|
LatestChickinApproval *Approval `gorm:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ type Recording struct {
|
|||||||
CumIntake *int `gorm:"column:cum_intake"`
|
CumIntake *int `gorm:"column:cum_intake"`
|
||||||
FcrValue *float64 `gorm:"column:fcr_value"`
|
FcrValue *float64 `gorm:"column:fcr_value"`
|
||||||
TotalChickQty *float64 `gorm:"column:total_chick_qty"`
|
TotalChickQty *float64 `gorm:"column:total_chick_qty"`
|
||||||
HandDay *float64 `gorm:"column:hand_day"`
|
HenDay *float64 `gorm:"column:hen_day"`
|
||||||
HandHouse *float64 `gorm:"column:hand_house"`
|
HenHouse *float64 `gorm:"column:hen_house"`
|
||||||
FeedIntake *float64 `gorm:"column:feed_intake"`
|
FeedIntake *float64 `gorm:"column:feed_intake"`
|
||||||
EggMesh *float64 `gorm:"column:egg_mesh"`
|
EggMass *float64 `gorm:"column:egg_mass"`
|
||||||
EggWeight *float64 `gorm:"column:egg_weight"`
|
EggWeight *float64 `gorm:"column:egg_weight"`
|
||||||
CreatedBy uint `gorm:"column:created_by"`
|
CreatedBy uint `gorm:"column:created_by"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
@@ -34,11 +34,11 @@ type Recording struct {
|
|||||||
|
|
||||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
|
|
||||||
StandardHandDay *float64 `gorm:"-"`
|
StandardHenDay *float64 `gorm:"-"`
|
||||||
StandardHandHouse *float64 `gorm:"-"`
|
StandardHenHouse *float64 `gorm:"-"`
|
||||||
StandardFeedIntake *float64 `gorm:"-"`
|
StandardFeedIntake *float64 `gorm:"-"`
|
||||||
StandardMaxDepletion *float64 `gorm:"-"`
|
StandardMaxDepletion *float64 `gorm:"-"`
|
||||||
StandardEggMesh *float64 `gorm:"-"`
|
StandardEggMass *float64 `gorm:"-"`
|
||||||
StandardEggWeight *float64 `gorm:"-"`
|
StandardEggWeight *float64 `gorm:"-"`
|
||||||
StandardFcr *float64 `gorm:"-"`
|
StandardFcr *float64 `gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ type RecordingEgg struct {
|
|||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
ProductFlagName *string `gorm:"column:product_flag_name" json:"-"`
|
ProductFlagName *string `gorm:"->;column:product_flag_name" json:"-"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,27 +8,22 @@ type StockTransferDetail struct {
|
|||||||
StockTransferId uint64
|
StockTransferId uint64
|
||||||
ProductId uint64
|
ProductId uint64
|
||||||
|
|
||||||
// === FIFO FIELDS - SOURCE WAREHOUSE (Usable) ===
|
|
||||||
// Tracking stock yang DIAMBIL dari source warehouse
|
|
||||||
SourceProductWarehouseID *uint64 `gorm:"column:source_product_warehouse_id"`
|
SourceProductWarehouseID *uint64 `gorm:"column:source_product_warehouse_id"`
|
||||||
UsageQty float64 `gorm:"column:usage_qty;default:0"` // Actual yang berhasil diambil
|
UsageQty float64 `gorm:"column:usage_qty;default:0"` // Actual yang berhasil diambil
|
||||||
PendingQty float64 `gorm:"column:pending_qty;default:0"` // Yang pending (nunggu stock)
|
PendingQty float64 `gorm:"column:pending_qty;default:0"` // Yang pending (nunggu stock)
|
||||||
|
DestProductWarehouseID *uint64 `gorm:"column:dest_product_warehouse_id"`
|
||||||
// === FIFO FIELDS - DESTINATION WAREHOUSE (Stockable) ===
|
TotalQty float64 `gorm:"column:total_qty;default:0"` // Total lot yang tersedia
|
||||||
// Tracking stock yang DITAMBAHKAN ke destination warehouse
|
TotalUsed float64 `gorm:"column:total_used;default:0"` // Yang sudah dipakai dari lot ini
|
||||||
DestProductWarehouseID *uint64 `gorm:"column:dest_product_warehouse_id"`
|
ExpenseNonstockId *uint64 `gorm:"column:expense_nonstock_id"`
|
||||||
TotalQty float64 `gorm:"column:total_qty;default:0"` // Total lot yang tersedia
|
CreatedAt time.Time
|
||||||
TotalUsed float64 `gorm:"column:total_used;default:0"` // Yang sudah dipakai dari lot ini
|
UpdatedAt time.Time
|
||||||
|
DeletedAt *time.Time `gorm:"index"`
|
||||||
// === METADATA ===
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
DeletedAt *time.Time `gorm:"index"`
|
|
||||||
|
|
||||||
// === RELATIONS ===
|
// === RELATIONS ===
|
||||||
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
|
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
|
||||||
Product *Product `gorm:"foreignKey:ProductId"`
|
Product *Product `gorm:"foreignKey:ProductId"`
|
||||||
SourceProductWarehouse *ProductWarehouse `gorm:"foreignKey:SourceProductWarehouseID"`
|
SourceProductWarehouse *ProductWarehouse `gorm:"foreignKey:SourceProductWarehouseID"`
|
||||||
DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID"`
|
DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID"`
|
||||||
|
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
|
||||||
DeliveryItems []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDetailId"`
|
DeliveryItems []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDetailId"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
|
const(
|
||||||
|
P_DashboardGetAll = "lti.dashboard.list"
|
||||||
|
)
|
||||||
// project-flock
|
// project-flock
|
||||||
const (
|
const (
|
||||||
P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing"
|
P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing"
|
||||||
@@ -44,7 +47,9 @@ const (
|
|||||||
P_ReportExpenseGetAll = "lti.repport.expense.list"
|
P_ReportExpenseGetAll = "lti.repport.expense.list"
|
||||||
P_ReportDeliveryGetAll = "lti.repport.delivery.list"
|
P_ReportDeliveryGetAll = "lti.repport.delivery.list"
|
||||||
P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list"
|
P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list"
|
||||||
|
P_ReportDebtSupplierGetAll = "lti.repport.debtsupplier.list"
|
||||||
P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list"
|
P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list"
|
||||||
|
P_ReportProductionResultGetAll = "lti.repport.production_result.list"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -134,18 +139,18 @@ const (
|
|||||||
P_NonstocksUpdateOne = "lti.master.nonstocks.update"
|
P_NonstocksUpdateOne = "lti.master.nonstocks.update"
|
||||||
P_NonstocksDeleteOne = "lti.master.nonstocks.delete"
|
P_NonstocksDeleteOne = "lti.master.nonstocks.delete"
|
||||||
|
|
||||||
P_ProductCategoriesGetAll = "lti.master.Product_categories.list"
|
P_ProductCategoriesGetAll = "lti.master.product_categories.list"
|
||||||
P_ProductCategoriesGetOne = "lti.master.Product_categories.detail"
|
P_ProductCategoriesGetOne = "lti.master.product_categories.detail"
|
||||||
P_ProductCategoriesCreateOne = "lti.master.Product_categories.create"
|
P_ProductCategoriesCreateOne = "lti.master.product_categories.create"
|
||||||
P_ProductCategoriesUpdateOne = "lti.master.Product_categories.update"
|
P_ProductCategoriesUpdateOne = "lti.master.product_categories.update"
|
||||||
P_ProductCategoriesDeleteOne = "lti.master.Product_categories.delete"
|
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_SuppliersGetAll = "lti.master.suppliers.list"
|
||||||
P_SuppliersGetOne = "lti.master.suppliers.detail"
|
P_SuppliersGetOne = "lti.master.suppliers.detail"
|
||||||
P_SuppliersCreateOne = "lti.master.suppliers.create"
|
P_SuppliersCreateOne = "lti.master.suppliers.create"
|
||||||
@@ -207,15 +212,15 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
P_PurchaseGetAll = "lti.Purchase.list"
|
P_PurchaseGetAll = "lti.purchase.list"
|
||||||
P_PurchaseGetOne = "lti.Purchase.detail"
|
P_PurchaseGetOne = "lti.purchase.detail"
|
||||||
P_PurchaseCreateOne = "lti.Purchase.create"
|
P_PurchaseCreateOne = "lti.purchase.create"
|
||||||
P_PurchaseUpdateOne = "lti.Purchase.update"
|
P_PurchaseUpdateOne = "lti.purchase.update"
|
||||||
P_PurchaseDeleteOne = "lti.Purchase.delete"
|
P_PurchaseDeleteOne = "lti.purchase.delete"
|
||||||
P_PurchaseItemDeleteOne = "lti.Purchase.delete.item"
|
P_PurchaseItemDeleteOne = "lti.purchase.delete.item"
|
||||||
P_PurchaseReceive = "lti.Purchase.receive"
|
P_PurchaseReceive = "lti.purchase.receive"
|
||||||
P_PurchaseApprovalStaff = "lti.Purchase.approve.staff"
|
P_PurchaseApprovalStaff = "lti.purchase.approve.staff"
|
||||||
P_PurchaseApprovalManager = "lti.Purchase.approve.manager"
|
P_PurchaseApprovalManager = "lti.purchase.approve.manager"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
|
func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
|
||||||
param := c.Params("project_flock_id")
|
param := c.Params("projectFlockId")
|
||||||
|
|
||||||
projectFlockID, err := strconv.Atoi(param)
|
projectFlockID, err := strconv.Atoi(param)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -55,16 +55,21 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
|||||||
kandang = &mapped
|
kandang = &mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var realizationDate time.Time
|
||||||
|
if e.DeliveryDate != nil {
|
||||||
|
realizationDate = *e.DeliveryDate
|
||||||
|
}
|
||||||
|
|
||||||
doNumber := deliveryOrdersDTO.GenerateDeliveryOrderNumber(e.MarketingProduct.Marketing.SoNumber, e.DeliveryDate, e.MarketingProduct.ProductWarehouse.Warehouse.Id)
|
doNumber := deliveryOrdersDTO.GenerateDeliveryOrderNumber(e.MarketingProduct.Marketing.SoNumber, e.DeliveryDate, e.MarketingProduct.ProductWarehouse.Warehouse.Id)
|
||||||
|
|
||||||
return SalesDTO{
|
return SalesDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
RealizationDate: *e.DeliveryDate,
|
RealizationDate: realizationDate,
|
||||||
Age: age,
|
Age: age,
|
||||||
DoNumber: doNumber,
|
DoNumber: doNumber,
|
||||||
Product: product,
|
Product: product,
|
||||||
Customer: customer,
|
Customer: customer,
|
||||||
Qty: e.UsageQty, // Show allocated quantity from FIFO
|
Qty: e.UsageQty,
|
||||||
Weight: e.TotalWeight,
|
Weight: e.TotalWeight,
|
||||||
AvgWeight: e.AvgWeight,
|
AvgWeight: e.AvgWeight,
|
||||||
Price: e.UnitPrice,
|
Price: e.UnitPrice,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -914,9 +915,8 @@ func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.C
|
|||||||
|
|
||||||
var rows []ActualUsageCostRow
|
var rows []ActualUsageCostRow
|
||||||
|
|
||||||
// Part 1: Get usage from recording_stocks (PAKAN, OVK, Vitamin, Obat, Kimia, dll)
|
purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
|
||||||
purchaseStockableKey := "PURCHASE_ITEMS"
|
transferStockableKey := fifo.StockableKeyStockTransferIn.String()
|
||||||
transferStockableKey := "STOCK_TRANSFER_DETAILS"
|
|
||||||
|
|
||||||
recordingQuery := db.
|
recordingQuery := db.
|
||||||
Table("recordings AS r").
|
Table("recordings AS r").
|
||||||
@@ -982,7 +982,6 @@ func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.C
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Part 2: Get usage from project_chickins (DOC, Pullet)
|
|
||||||
chickinQuery := db.
|
chickinQuery := db.
|
||||||
Table("project_chickins AS pc").
|
Table("project_chickins AS pc").
|
||||||
Select(`
|
Select(`
|
||||||
@@ -1006,7 +1005,6 @@ func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.C
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge results
|
|
||||||
rows = append(rows, chickinRows...)
|
rows = append(rows, chickinRows...)
|
||||||
|
|
||||||
return rows, nil
|
return rows, nil
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.Cl
|
|||||||
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withClosingRelations(db)
|
db = s.withClosingRelations(db)
|
||||||
if params.Search != "" {
|
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")
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
})
|
})
|
||||||
@@ -151,9 +151,19 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entit
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(realisasi) == 0 {
|
if len(realisasi) == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Penjualan realisasi not found")
|
return []entity.MarketingDeliveryProduct{}, nil
|
||||||
}
|
}
|
||||||
return realisasi, 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) {
|
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) {
|
||||||
@@ -403,18 +413,9 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.Ove
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) {
|
func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) {
|
||||||
if projectFlockID == 0 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: func(ctx context.Context, id uint) (bool, error) {
|
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
|
||||||
_, err := s.ProjectFlockRepo.GetByID(ctx, id, nil)
|
|
||||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return err == nil, err
|
|
||||||
}},
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -429,13 +430,11 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*
|
|||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get actual usage cost instead of purchase items
|
|
||||||
actualUsageRows, err := s.Repository.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID)
|
actualUsageRows, err := s.Repository.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert actual usage rows to pseudo purchase items
|
|
||||||
purchaseItems := s.convertActualUsageToPurchaseItems(c.Context(), actualUsageRows)
|
purchaseItems := s.convertActualUsageToPurchaseItems(c.Context(), actualUsageRows)
|
||||||
|
|
||||||
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/dto"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations"
|
||||||
|
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -28,6 +29,18 @@ func (u *DailyChecklistController) GetAll(c *fiber.Ctx) error {
|
|||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
}
|
}
|
||||||
|
query.DateFrom = c.Query("date_from", "")
|
||||||
|
query.DateTo = c.Query("date_to", "")
|
||||||
|
query.Status = c.Query("status", "")
|
||||||
|
|
||||||
|
if kandangParam := c.Query("kandang_id", ""); kandangParam != "" {
|
||||||
|
kandangID, err := strconv.ParseUint(kandangParam, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
|
||||||
|
}
|
||||||
|
value := uint(kandangID)
|
||||||
|
query.KandangID = &value
|
||||||
|
}
|
||||||
|
|
||||||
if query.Page < 1 || query.Limit < 1 {
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
@@ -38,6 +51,41 @@ func (u *DailyChecklistController) GetAll(c *fiber.Ctx) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
responseData := make([]dto.DailyChecklistListDTO, len(result))
|
||||||
|
for i, item := range result {
|
||||||
|
var name string
|
||||||
|
if item.Name != nil {
|
||||||
|
name = *item.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
var status string
|
||||||
|
if item.Status != nil {
|
||||||
|
status = *item.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
var kandang *kandangDTO.KandangRelationDTO
|
||||||
|
if item.Kandang.Id != 0 {
|
||||||
|
mapped := kandangDTO.ToKandangRelationDTO(item.Kandang)
|
||||||
|
kandang = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData[i] = dto.DailyChecklistListDTO{
|
||||||
|
Id: item.ID,
|
||||||
|
Name: name,
|
||||||
|
Status: status,
|
||||||
|
Category: item.Category,
|
||||||
|
RejectReason: item.RejectReason,
|
||||||
|
Date: item.Date,
|
||||||
|
Kandang: kandang,
|
||||||
|
CreatedUser: nil,
|
||||||
|
CreatedAt: item.CreatedAt,
|
||||||
|
UpdatedAt: item.UpdatedAt,
|
||||||
|
TotalPhase: item.TotalPhase,
|
||||||
|
TotalActivity: item.TotalActivity,
|
||||||
|
Progress: item.Progress,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
return c.Status(fiber.StatusOK).
|
||||||
JSON(response.SuccessWithPaginate[dto.DailyChecklistListDTO]{
|
JSON(response.SuccessWithPaginate[dto.DailyChecklistListDTO]{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
@@ -49,29 +97,233 @@ func (u *DailyChecklistController) GetAll(c *fiber.Ctx) error {
|
|||||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
TotalResults: totalResults,
|
TotalResults: totalResults,
|
||||||
},
|
},
|
||||||
Data: dto.ToDailyChecklistListDTOs(result),
|
Data: responseData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *DailyChecklistController) GetSummary(c *fiber.Ctx) error {
|
||||||
|
query := &validation.SummaryQuery{
|
||||||
|
DateFrom: c.Query("date_from"),
|
||||||
|
DateTo: c.Query("date_to"),
|
||||||
|
Category: c.Query("category"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.DateFrom == "" || query.DateTo == "" {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "date_from and date_to are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if kandangParam := c.Query("kandang_id"); kandangParam != "" {
|
||||||
|
kandangID, err := strconv.ParseUint(kandangParam, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
|
||||||
|
}
|
||||||
|
value := uint(kandangID)
|
||||||
|
query.KandangID = &value
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.DailyChecklistService.GetSummary(c, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type summaryResponse struct {
|
||||||
|
PerformanceOverview []dto.DailyChecklistPerformanceOverviewDTO `json:"performance_overview"`
|
||||||
|
TrackingABK []dto.DailyChecklistSummaryDTO `json:"tracking_abk"`
|
||||||
|
}
|
||||||
|
|
||||||
|
performanceMap := make(map[uint]*dto.DailyChecklistPerformanceOverviewDTO)
|
||||||
|
tracking := make([]dto.DailyChecklistSummaryDTO, len(result))
|
||||||
|
|
||||||
|
for i, summary := range result {
|
||||||
|
tracking[i] = dto.DailyChecklistSummaryDTO{
|
||||||
|
EmployeeID: summary.EmployeeID,
|
||||||
|
EmployeeName: summary.EmployeeName,
|
||||||
|
KandangID: summary.KandangID,
|
||||||
|
KandangName: summary.KandangName,
|
||||||
|
TotalActivity: summary.TotalActivity,
|
||||||
|
ActivityDone: summary.ActivityDone,
|
||||||
|
ActivityLeft: summary.ActivityLeft,
|
||||||
|
CompletionRate: summary.CompletionRate,
|
||||||
|
LastActivity: summary.LastActivity,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := performanceMap[summary.EmployeeID]; !ok {
|
||||||
|
performanceMap[summary.EmployeeID] = &dto.DailyChecklistPerformanceOverviewDTO{
|
||||||
|
EmployeeID: summary.EmployeeID,
|
||||||
|
EmployeeName: summary.EmployeeName,
|
||||||
|
Kandang: dto.DailyChecklistReportEntityDTO{
|
||||||
|
Id: summary.KandangID,
|
||||||
|
Name: summary.KandangName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
performanceMap[summary.EmployeeID].TotalActivity += summary.TotalActivity
|
||||||
|
performanceMap[summary.EmployeeID].ActivityDone += summary.ActivityDone
|
||||||
|
performanceMap[summary.EmployeeID].ActivityLeft += summary.ActivityLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
performance := make([]dto.DailyChecklistPerformanceOverviewDTO, 0, len(performanceMap))
|
||||||
|
for _, v := range performanceMap {
|
||||||
|
performance = append(performance, *v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get daily checklist summary successfully",
|
||||||
|
Data: summaryResponse{
|
||||||
|
PerformanceOverview: performance,
|
||||||
|
TrackingABK: tracking,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DailyChecklistController) GetReport(c *fiber.Ctx) error {
|
||||||
|
query := &validation.ReportQuery{
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
Month: c.QueryInt("bulan", 0),
|
||||||
|
Year: c.QueryInt("tahun", 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
parseUintParam := func(param string) (*uint, error) {
|
||||||
|
if param == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
value, err := strconv.ParseUint(param, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
u := uint(value)
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, err := parseUintParam(c.Query("area_id", "")); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid area_id")
|
||||||
|
} else {
|
||||||
|
query.AreaID = val
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, err := parseUintParam(c.Query("location_id", "")); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid location_id")
|
||||||
|
} else {
|
||||||
|
query.LocationID = val
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, err := parseUintParam(c.Query("kandang_id", "")); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
|
||||||
|
} else {
|
||||||
|
query.KandangID = val
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, err := parseUintParam(c.Query("employee_id", "")); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid employee_id")
|
||||||
|
} else {
|
||||||
|
query.EmployeeID = val
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, err := parseUintParam(c.Query("phase_id", "")); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid phase_id")
|
||||||
|
} else {
|
||||||
|
query.PhaseID = val
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Month == 0 || query.Year == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "bulan and tahun are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, totalResults, err := u.DailyChecklistService.GetReport(c, query)
|
||||||
|
withoutActivities := func(src map[string]int) map[string]int {
|
||||||
|
if src == nil {
|
||||||
|
return map[string]int{}
|
||||||
|
}
|
||||||
|
return src
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData := make([]dto.DailyChecklistReportDTO, len(result))
|
||||||
|
for i, item := range result {
|
||||||
|
responseData[i] = dto.DailyChecklistReportDTO{
|
||||||
|
Area: dto.DailyChecklistReportEntityDTO{
|
||||||
|
Id: item.AreaID,
|
||||||
|
Name: item.AreaName,
|
||||||
|
},
|
||||||
|
Farm: dto.DailyChecklistReportEntityDTO{
|
||||||
|
Id: item.LocationID,
|
||||||
|
Name: item.LocationName,
|
||||||
|
},
|
||||||
|
Kandang: dto.DailyChecklistReportEntityDTO{
|
||||||
|
Id: item.KandangID,
|
||||||
|
Name: item.KandangName,
|
||||||
|
},
|
||||||
|
ABK: dto.DailyChecklistReportEntityDTO{
|
||||||
|
Id: item.EmployeeID,
|
||||||
|
Name: item.EmployeeName,
|
||||||
|
},
|
||||||
|
Phase: item.PhaseName,
|
||||||
|
DailyActivities: withoutActivities(item.DailyActivities),
|
||||||
|
Summary: dto.DailyChecklistReportSummaryDTO{
|
||||||
|
TotalChecklist: item.Summary.TotalChecklist,
|
||||||
|
JumlahHariEfektif: item.Summary.JumlahHariEfektif,
|
||||||
|
AbkPercentage: item.Summary.AbkPercentage,
|
||||||
|
KandangPercentage: item.Summary.KandangPercentage,
|
||||||
|
Kategori: dto.DailyChecklistReportCategoryDTO{
|
||||||
|
Kurang: item.Summary.Category.Kurang,
|
||||||
|
Cukup: item.Summary.Category.Cukup,
|
||||||
|
Baik: item.Summary.Category.Baik,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.SuccessWithPaginate[dto.DailyChecklistReportDTO]{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get daily checklist report successfully",
|
||||||
|
Meta: response.Meta{
|
||||||
|
Page: query.Page,
|
||||||
|
Limit: query.Limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
TotalResults: totalResults,
|
||||||
|
},
|
||||||
|
Data: responseData,
|
||||||
|
})
|
||||||
|
}
|
||||||
func (u *DailyChecklistController) GetOne(c *fiber.Ctx) error {
|
func (u *DailyChecklistController) GetOne(c *fiber.Ctx) error {
|
||||||
param := c.Params("id")
|
param := c.Params("idDailyChecklist")
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
id, err := strconv.Atoi(param)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.DailyChecklistService.GetOne(c, uint(id))
|
detail, err := u.DailyChecklistService.GetDetail(c, uint(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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).
|
return c.Status(fiber.StatusOK).
|
||||||
JSON(response.Success{
|
JSON(response.Success{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get dailyChecklist successfully",
|
Message: "Get dailyChecklist successfully",
|
||||||
Data: dto.ToDailyChecklistListDTO(*result),
|
Data: dto.ToDailyChecklistDetailDTO(detail.Checklist, detail.Phases, detail.Tasks, detail.AssignedEmployees, detail.TotalActivities, detail.Progress, documentDTOs),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,13 +350,19 @@ func (u *DailyChecklistController) CreateOne(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
func (u *DailyChecklistController) UpdateOne(c *fiber.Ctx) error {
|
func (u *DailyChecklistController) UpdateOne(c *fiber.Ctx) error {
|
||||||
req := new(validation.Update)
|
req := new(validation.Update)
|
||||||
param := c.Params("id")
|
param := c.Params("idDailyChecklist")
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
id, err := strconv.Atoi(param)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
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 {
|
if err := c.BodyParser(req); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
}
|
}
|
||||||
@@ -124,7 +382,7 @@ func (u *DailyChecklistController) UpdateOne(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *DailyChecklistController) DeleteOne(c *fiber.Ctx) error {
|
func (u *DailyChecklistController) DeleteOne(c *fiber.Ctx) error {
|
||||||
param := c.Params("id")
|
param := c.Params("idDailyChecklist")
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
id, err := strconv.Atoi(param)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -217,6 +475,32 @@ func (u *DailyChecklistController) RemoveAssignment(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *DailyChecklistController) GetPhaseByIdChecklist(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("idDailyChecklist")
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid daily checklist id")
|
||||||
|
}
|
||||||
|
|
||||||
|
phaseIDs, err := u.DailyChecklistService.GetChecklistPhaseIDs(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData := make([]map[string]uint, len(phaseIDs))
|
||||||
|
for i, phaseID := range phaseIDs {
|
||||||
|
responseData[i] = map[string]uint{"phase_id": phaseID}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get phases successfully",
|
||||||
|
Data: responseData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *DailyChecklistController) GetAllTasks(c *fiber.Ctx) error {
|
func (u *DailyChecklistController) GetAllTasks(c *fiber.Ctx) error {
|
||||||
checklistParam := c.Query("checklist_id", "")
|
checklistParam := c.Query("checklist_id", "")
|
||||||
if checklistParam == "" {
|
if checklistParam == "" {
|
||||||
@@ -241,3 +525,21 @@ func (u *DailyChecklistController) GetAllTasks(c *fiber.Ctx) error {
|
|||||||
Data: result,
|
Data: result,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *DailyChecklistController) UpdateAssignment(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.UpdateAssignment)
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.DailyChecklistService.UpdateAssignment(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Assignment updated successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
employeeDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/dto"
|
||||||
|
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||||
|
phaseActivityDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/dto"
|
||||||
|
phasesDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/dto"
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,15 +19,110 @@ type DailyChecklistRelationDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DailyChecklistListDTO struct {
|
type DailyChecklistListDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
Status string `json:"status"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
Category string `json:"category"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
Date time.Time `json:"date"`
|
||||||
|
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
||||||
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
TotalPhase int `json:"total_phase"`
|
||||||
|
TotalActivity int `json:"total_activity"`
|
||||||
|
Progress int `json:"progress"`
|
||||||
|
RejectReason *string `json:"reject_reason"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DailyChecklistDetailDTO struct {
|
type DailyChecklistDetailDTO struct {
|
||||||
DailyChecklistListDTO
|
DailyChecklistListDTO
|
||||||
|
Phases []DailyChecklistPhaseDTO `json:"phases"`
|
||||||
|
Tasks []DailyChecklistActivityTaskDTO `json:"tasks"`
|
||||||
|
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 {
|
||||||
|
EmployeeID uint `json:"employee_id"`
|
||||||
|
EmployeeName string `json:"employee_name"`
|
||||||
|
KandangID uint `json:"kandang_id"`
|
||||||
|
KandangName string `json:"kandang_name"`
|
||||||
|
TotalActivity int `json:"total_activity"`
|
||||||
|
ActivityDone int `json:"activity_done"`
|
||||||
|
ActivityLeft int `json:"activity_left"`
|
||||||
|
CompletionRate int `json:"completion_rate"`
|
||||||
|
LastActivity *time.Time `json:"last_activity,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistPerformanceOverviewDTO struct {
|
||||||
|
EmployeeID uint `json:"employee_id"`
|
||||||
|
EmployeeName string `json:"employee_name"`
|
||||||
|
Kandang DailyChecklistReportEntityDTO `json:"kandang"`
|
||||||
|
TotalActivity int `json:"total_activity"`
|
||||||
|
ActivityDone int `json:"activity_done"`
|
||||||
|
ActivityLeft int `json:"activity_left"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistReportDTO struct {
|
||||||
|
Area DailyChecklistReportEntityDTO `json:"area"`
|
||||||
|
Farm DailyChecklistReportEntityDTO `json:"farm"`
|
||||||
|
Kandang DailyChecklistReportEntityDTO `json:"kandang"`
|
||||||
|
ABK DailyChecklistReportEntityDTO `json:"abk"`
|
||||||
|
Phase string `json:"phase"`
|
||||||
|
DailyActivities map[string]int `json:"daily_activities"`
|
||||||
|
Summary DailyChecklistReportSummaryDTO `json:"summary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistReportEntityDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistReportSummaryDTO struct {
|
||||||
|
TotalChecklist int `json:"total_checklist"`
|
||||||
|
JumlahHariEfektif int `json:"jumlah_hari_efektif"`
|
||||||
|
AbkPercentage int `json:"abk_percentage"`
|
||||||
|
KandangPercentage int `json:"kandang_percentage"`
|
||||||
|
Kategori DailyChecklistReportCategoryDTO `json:"kategori"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistReportCategoryDTO struct {
|
||||||
|
Kurang int `json:"kurang"`
|
||||||
|
Cukup int `json:"cukup"`
|
||||||
|
Baik int `json:"baik"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistPhaseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
PhaseId uint `json:"phase_id"`
|
||||||
|
Phase phasesDTO.PhasesListDTO `json:"phase"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistActivityTaskDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
ChecklistId uint `json:"checklist_id"`
|
||||||
|
PhaseId uint `json:"phase_id"`
|
||||||
|
PhaseActivityId uint `json:"phase_activity_id"`
|
||||||
|
TimeType *string `json:"time_type"`
|
||||||
|
Notes *string `json:"notes"`
|
||||||
|
Phase phasesDTO.PhasesListDTO `json:"phase"`
|
||||||
|
PhaseActivity phaseActivityDTO.PhaseActivityListDTO `json:"phase_activity"`
|
||||||
|
Assignments []DailyChecklistAssignmentDTO `json:"assignments"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyChecklistAssignmentDTO struct {
|
||||||
|
Employee employeeDTO.EmployeesRelationDTO `json:"employee"`
|
||||||
|
Checked bool `json:"checked"`
|
||||||
|
Note *string `json:"note"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
@@ -52,25 +151,94 @@ func ToDailyChecklistListDTO(e entity.DailyChecklist) DailyChecklistListDTO {
|
|||||||
name = *e.Name
|
name = *e.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var status string
|
||||||
|
if e.Status != nil {
|
||||||
|
status = *e.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
var kandang *kandangDTO.KandangRelationDTO
|
||||||
|
if e.Kandang.Id != 0 {
|
||||||
|
mapped := kandangDTO.ToKandangRelationDTO(e.Kandang)
|
||||||
|
kandang = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
return DailyChecklistListDTO{
|
return DailyChecklistListDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
Name: name,
|
Name: name,
|
||||||
CreatedAt: e.CreatedAt,
|
Status: status,
|
||||||
UpdatedAt: e.UpdatedAt,
|
Category: e.Category,
|
||||||
CreatedUser: createdUser,
|
Date: e.Date,
|
||||||
|
Kandang: kandang,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
CreatedUser: createdUser,
|
||||||
|
TotalPhase: 0,
|
||||||
|
TotalActivity: 0,
|
||||||
|
Progress: 0,
|
||||||
|
RejectReason: e.RejectReason,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToDailyChecklistListDTOs(e []entity.DailyChecklist) []DailyChecklistListDTO {
|
func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity.DailyChecklistPhase, tasks []entity.DailyChecklistActivityTask, assignedEmployees []entity.Employee, totalActivities int, progress float64, documentURLs []DailyChecklistDocumentDTO) DailyChecklistDetailDTO {
|
||||||
result := make([]DailyChecklistListDTO, len(e))
|
phaseDTOs := make([]DailyChecklistPhaseDTO, 0, len(phases))
|
||||||
for i, r := range e {
|
for _, phase := range phases {
|
||||||
result[i] = ToDailyChecklistListDTO(r)
|
phaseDTOs = append(phaseDTOs, DailyChecklistPhaseDTO{
|
||||||
|
Id: phase.Id,
|
||||||
|
PhaseId: phase.PhaseId,
|
||||||
|
Phase: phasesDTO.ToPhasesListDTO(phase.Phase),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
taskDTOs := make([]DailyChecklistActivityTaskDTO, 0, len(tasks))
|
||||||
|
for _, task := range tasks {
|
||||||
|
mappedAssignments := make([]DailyChecklistAssignmentDTO, 0, len(task.Assignments))
|
||||||
|
for _, assignment := range task.Assignments {
|
||||||
|
if assignment.Employee.Id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mapped := DailyChecklistAssignmentDTO{
|
||||||
|
Employee: employeeDTO.ToEmployeesRelationDTO(assignment.Employee),
|
||||||
|
Checked: assignment.Checked,
|
||||||
|
Note: assignment.Note,
|
||||||
|
}
|
||||||
|
mappedAssignments = append(mappedAssignments, mapped)
|
||||||
|
}
|
||||||
|
|
||||||
|
phaseDTO := phasesDTO.PhasesListDTO{}
|
||||||
|
if task.Phase.Id != 0 {
|
||||||
|
phaseDTO = phasesDTO.ToPhasesListDTO(task.Phase)
|
||||||
|
}
|
||||||
|
|
||||||
|
activityDTO := phaseActivityDTO.PhaseActivityListDTO{}
|
||||||
|
if task.PhaseActivity.Id != 0 {
|
||||||
|
activityDTO = phaseActivityDTO.ToPhaseActivityListDTO(task.PhaseActivity)
|
||||||
|
}
|
||||||
|
|
||||||
|
taskDTOs = append(taskDTOs, DailyChecklistActivityTaskDTO{
|
||||||
|
Id: task.Id,
|
||||||
|
ChecklistId: task.ChecklistId,
|
||||||
|
PhaseId: task.PhaseId,
|
||||||
|
PhaseActivityId: task.PhaseActivityId,
|
||||||
|
TimeType: task.TimeType,
|
||||||
|
Notes: task.Notes,
|
||||||
|
Phase: phaseDTO,
|
||||||
|
PhaseActivity: activityDTO,
|
||||||
|
Assignments: mappedAssignments,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
assignedDTOs := make([]employeeDTO.EmployeesRelationDTO, 0, len(assignedEmployees))
|
||||||
|
for _, emp := range assignedEmployees {
|
||||||
|
assignedDTOs = append(assignedDTOs, employeeDTO.ToEmployeesRelationDTO(emp))
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToDailyChecklistDetailDTO(e entity.DailyChecklist) DailyChecklistDetailDTO {
|
|
||||||
return DailyChecklistDetailDTO{
|
return DailyChecklistDetailDTO{
|
||||||
DailyChecklistListDTO: ToDailyChecklistListDTO(e),
|
DailyChecklistListDTO: ToDailyChecklistListDTO(checklist),
|
||||||
|
Phases: phaseDTOs,
|
||||||
|
Tasks: taskDTOs,
|
||||||
|
AssignedEmployees: assignedDTOs,
|
||||||
|
TotalActivity: totalActivities,
|
||||||
|
Progress: progress,
|
||||||
|
DocumentURLs: documentURLs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
package dailyChecklists
|
package dailyChecklists
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"gorm.io/gorm"
|
"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"
|
rDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories"
|
||||||
sDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
|
sDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
|
||||||
rPhases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
|
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)
|
dailyChecklistRepo := rDailyChecklist.NewDailyChecklistRepository(db)
|
||||||
phasesRepo := rPhases.NewPhasesRepository(db)
|
phasesRepo := rPhases.NewPhasesRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(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)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
DailyChecklistRoutes(router, userService, dailyChecklistService)
|
DailyChecklistRoutes(router, userService, dailyChecklistService)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package dailyChecklists
|
package dailyChecklists
|
||||||
|
|
||||||
import (
|
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"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/controllers"
|
||||||
dailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
|
dailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,23 +13,51 @@ func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist.
|
|||||||
ctrl := controller.NewDailyChecklistController(s)
|
ctrl := controller.NewDailyChecklistController(s)
|
||||||
|
|
||||||
route := v1.Group("/daily-checklists")
|
route := v1.Group("/daily-checklists")
|
||||||
route.Use(m.Auth(u))
|
// route.Use(m.Auth(u))
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
|
route.Get("/report", ctrl.GetReport)
|
||||||
|
|
||||||
|
route.Get("/summary", ctrl.GetSummary)
|
||||||
|
|
||||||
|
route.Get("/report", ctrl.GetReport)
|
||||||
|
|
||||||
|
// create daily checklist
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
|
||||||
|
// get detail data daily checklist by id
|
||||||
|
route.Get("/relation/:idDailyChecklist", ctrl.GetOne)
|
||||||
|
|
||||||
|
// get phases by daily checklist id
|
||||||
|
route.Get("/phase/:idDailyChecklist", ctrl.GetPhaseByIdChecklist)
|
||||||
|
|
||||||
// create task
|
// create task
|
||||||
|
/*
|
||||||
|
ketika add phase
|
||||||
|
*/
|
||||||
route.Post("/phase/:idDailyChecklist", ctrl.CreateDailyChecklistPhase)
|
route.Post("/phase/:idDailyChecklist", ctrl.CreateDailyChecklistPhase)
|
||||||
|
|
||||||
// create assigment
|
// create assigment
|
||||||
|
/*
|
||||||
|
ketika add ABK
|
||||||
|
*/
|
||||||
route.Post("/assignment/:idDailyChecklist", ctrl.CreateAssignment)
|
route.Post("/assignment/:idDailyChecklist", ctrl.CreateAssignment)
|
||||||
|
|
||||||
// remove assignment
|
// remove assignment
|
||||||
|
/*
|
||||||
|
ketika remove ABK
|
||||||
|
*/
|
||||||
route.Delete("/:idDailyChecklist/assignments/:idEmployee", ctrl.RemoveAssignment)
|
route.Delete("/:idDailyChecklist/assignments/:idEmployee", ctrl.RemoveAssignment)
|
||||||
|
|
||||||
//get all tasks
|
//get all tasks
|
||||||
route.Get("/tasks", ctrl.GetAllTasks)
|
route.Get("/tasks", ctrl.GetAllTasks)
|
||||||
|
|
||||||
route.Get("/:id", ctrl.GetOne)
|
// update assignment
|
||||||
route.Patch("/:id", ctrl.UpdateOne)
|
/*
|
||||||
route.Delete("/:id", ctrl.DeleteOne)
|
ketika check dan uncheck tugas oleh ABK
|
||||||
|
*/
|
||||||
|
route.Post("/assignment", ctrl.UpdateAssignment)
|
||||||
|
|
||||||
|
route.Patch("/:idDailyChecklist", ctrl.UpdateOne)
|
||||||
|
route.Delete("/:idDailyChecklist", ctrl.DeleteOne)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,9 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mime/multipart"
|
||||||
|
)
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
Date string `json:"date" validate:"required"`
|
Date string `json:"date" validate:"required"`
|
||||||
KandangId uint `json:"kandang_id" validate:"required"`
|
KandangId uint `json:"kandang_id" validate:"required"`
|
||||||
@@ -8,13 +12,20 @@ type Create struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
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 {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
|
DateFrom string `query:"date_from" validate:"omitempty"`
|
||||||
|
DateTo string `query:"date_to" validate:"omitempty"`
|
||||||
|
Status string `query:"status" validate:"omitempty"`
|
||||||
|
KandangID *uint `query:"kandang_id" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AssignPhases struct {
|
type AssignPhases struct {
|
||||||
@@ -24,3 +35,29 @@ type AssignPhases struct {
|
|||||||
type AssignTask struct {
|
type AssignTask struct {
|
||||||
EmployeeIDs string `json:"employee_ids" validate:"required"`
|
EmployeeIDs string `json:"employee_ids" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdateAssignment struct {
|
||||||
|
TaskID uint `json:"task_id" validate:"required"`
|
||||||
|
EmployeeID uint `json:"employee_id" validate:"required"`
|
||||||
|
Checked *bool `json:"checked,omitempty"`
|
||||||
|
Note *string `json:"note,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SummaryQuery struct {
|
||||||
|
DateFrom string `query:"date_from" validate:"required"`
|
||||||
|
DateTo string `query:"date_to" validate:"required"`
|
||||||
|
Category string `query:"category" validate:"omitempty"`
|
||||||
|
KandangID *uint `query:"kandang_id" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
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"`
|
||||||
|
LocationID *uint `query:"location_id" validate:"omitempty"`
|
||||||
|
KandangID *uint `query:"kandang_id" validate:"omitempty"`
|
||||||
|
EmployeeID *uint `query:"employee_id" validate:"omitempty"`
|
||||||
|
PhaseID *uint `query:"phase_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
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
|
nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
|
||||||
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
@@ -32,6 +33,7 @@ type ExpenseBaseDTO struct {
|
|||||||
|
|
||||||
type ExpenseListDTO struct {
|
type ExpenseListDTO struct {
|
||||||
ExpenseBaseDTO
|
ExpenseBaseDTO
|
||||||
|
GrandTotal float64 `json:"grand_total"`
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -140,8 +142,11 @@ func ToExpenseListDTO(e entity.Expense) ExpenseListDTO {
|
|||||||
latestApproval = &mapped
|
latestApproval = &mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
|
grandTotal := calculateGrandTotal(&e)
|
||||||
|
|
||||||
return ExpenseListDTO{
|
return ExpenseListDTO{
|
||||||
ExpenseBaseDTO: ToExpenseBaseDTO(&e),
|
ExpenseBaseDTO: ToExpenseBaseDTO(&e),
|
||||||
|
GrandTotal: grandTotal,
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
UpdatedAt: e.UpdatedAt,
|
||||||
@@ -344,3 +349,25 @@ func ToKandangGroupDTO(pengajuans []ExpenseNonstockDTO, realisasi []ExpenseReali
|
|||||||
|
|
||||||
return kandangs
|
return kandangs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func calculateGrandTotal(expense *entity.Expense) float64 {
|
||||||
|
|
||||||
|
useRealization := expense.LatestApproval != nil && expense.LatestApproval.StepNumber >= uint16(utils.ExpenseStepRealisasi)
|
||||||
|
|
||||||
|
if useRealization {
|
||||||
|
|
||||||
|
var total float64
|
||||||
|
for _, ns := range expense.Nonstocks {
|
||||||
|
if ns.Realization != nil {
|
||||||
|
total += ns.Realization.Qty * ns.Realization.Price
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
var total float64
|
||||||
|
for _, ns := range expense.Nonstocks {
|
||||||
|
total += ns.Qty * ns.Price
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context
|
|||||||
Joins("LEFT JOIN suppliers ON suppliers.id = expenses.supplier_id")
|
Joins("LEFT JOIN suppliers ON suppliers.id = expenses.supplier_id")
|
||||||
|
|
||||||
if filters.Search != "" {
|
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+"%")
|
"%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
expenses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
if params.Search != "" {
|
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")
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
})
|
})
|
||||||
@@ -211,6 +211,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
|||||||
if !hasKandang && req.Category == string(utils.ExpenseCategoryBOP) {
|
if !hasKandang && req.Category == string(utils.ExpenseCategoryBOP) {
|
||||||
projectFlockRepoTx := projectFlockKandangRepo.NewProjectflockRepository(dbTransaction)
|
projectFlockRepoTx := projectFlockKandangRepo.NewProjectflockRepository(dbTransaction)
|
||||||
activeProjectFlocks, err := projectFlockRepoTx.GetActiveByLocationID(c.Context(), req.LocationID)
|
activeProjectFlocks, err := projectFlockRepoTx.GetActiveByLocationID(c.Context(), req.LocationID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get active project flocks for location")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get active project flocks for location")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,20 +101,25 @@ func ToInitialDetailDTO(e entity.Payment) InitialDetailDTO {
|
|||||||
|
|
||||||
func partyFromInitial(e entity.Payment) Party {
|
func partyFromInitial(e entity.Payment) Party {
|
||||||
party := Party{
|
party := Party{
|
||||||
Id: e.PartyId,
|
Id: e.PartyId,
|
||||||
Type: e.PartyType,
|
Type: e.PartyType,
|
||||||
|
}
|
||||||
|
if e.PartyAccountNumber != nil {
|
||||||
|
party.AccountNumber = *e.PartyAccountNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
switch utils.PaymentParty(e.PartyType) {
|
switch utils.PaymentParty(e.PartyType) {
|
||||||
case utils.PaymentPartyCustomer:
|
case utils.PaymentPartyCustomer:
|
||||||
if e.Customer != nil && e.Customer.Id != 0 {
|
if e.Customer != nil && e.Customer.Id != 0 {
|
||||||
party.Name = e.Customer.Name
|
party.Name = e.Customer.Name
|
||||||
party.AccountNumber = e.Customer.AccountNumber
|
if party.AccountNumber == "" {
|
||||||
|
party.AccountNumber = e.Customer.AccountNumber
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case utils.PaymentPartySupplier:
|
case utils.PaymentPartySupplier:
|
||||||
if e.Supplier != nil && e.Supplier.Id != 0 {
|
if e.Supplier != nil && e.Supplier.Id != 0 {
|
||||||
party.Name = e.Supplier.Name
|
party.Name = e.Supplier.Name
|
||||||
if e.Supplier.AccountNumber != nil {
|
if party.AccountNumber == "" && e.Supplier.AccountNumber != nil {
|
||||||
party.AccountNumber = *e.Supplier.AccountNumber
|
party.AccountNumber = *e.Supplier.AccountNumber
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ func (s *initialService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
|||||||
TransactionType: string(utils.TransactionTypeSaldoAwal),
|
TransactionType: string(utils.TransactionTypeSaldoAwal),
|
||||||
PartyType: party,
|
PartyType: party,
|
||||||
PartyId: req.PartyId,
|
PartyId: req.PartyId,
|
||||||
|
PartyAccountNumber: nil,
|
||||||
PaymentDate: time.Now(),
|
PaymentDate: time.Now(),
|
||||||
PaymentMethod: string(utils.PaymentMethodSaldo),
|
PaymentMethod: string(utils.PaymentMethodSaldo),
|
||||||
BankId: req.BankId,
|
BankId: req.BankId,
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ func (s *injectionService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
TransactionType: string(utils.TransactionTypeInjection),
|
TransactionType: string(utils.TransactionTypeInjection),
|
||||||
PartyType: string(utils.PaymentPartyCustomer),
|
PartyType: string(utils.PaymentPartyCustomer),
|
||||||
PartyId: 0,
|
PartyId: 0,
|
||||||
|
PartyAccountNumber: nil,
|
||||||
PaymentDate: adjustmentDate,
|
PaymentDate: adjustmentDate,
|
||||||
PaymentMethod: string(utils.PaymentMethodSaldo),
|
PaymentMethod: string(utils.PaymentMethodSaldo),
|
||||||
BankId: req.BankId,
|
BankId: req.BankId,
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
BankId *uint `json:"bank_id" validate:"required_strict,number,gt=0"`
|
BankId *uint `json:"bank_id" validate:"required_strict,number,gt=0"`
|
||||||
AdjustmentDate string `json:"adjustment_date" validate:"required_strict"`
|
AdjustmentDate string `json:"adjustment_date" validate:"required_strict"`
|
||||||
Nominal float64 `json:"nominal" validate:"required_strict,gt=0"`
|
Nominal float64 `json:"nominal" validate:"required_strict,gt=0"`
|
||||||
Notes string `json:"notes" validate:"required_strict,max=500"`
|
Notes string `json:"notes" validate:"required_strict,max=500"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
|
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||||
AdjustmentDate *string `json:"adjustment_date,omitempty" validate:"omitempty"`
|
AdjustmentDate *string `json:"adjustment_date,omitempty" validate:"omitempty"`
|
||||||
Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"`
|
Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"`
|
||||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
|
|||||||
@@ -124,20 +124,25 @@ func ToPaymentDetailDTO(e entity.Payment) PaymentDetailDTO {
|
|||||||
|
|
||||||
func partyFromPayment(e entity.Payment) Party {
|
func partyFromPayment(e entity.Payment) Party {
|
||||||
party := Party{
|
party := Party{
|
||||||
Id: e.PartyId,
|
Id: e.PartyId,
|
||||||
Type: e.PartyType,
|
Type: e.PartyType,
|
||||||
|
}
|
||||||
|
if e.PartyAccountNumber != nil {
|
||||||
|
party.AccountNumber = *e.PartyAccountNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
switch utils.PaymentParty(e.PartyType) {
|
switch utils.PaymentParty(e.PartyType) {
|
||||||
case utils.PaymentPartyCustomer:
|
case utils.PaymentPartyCustomer:
|
||||||
if e.Customer != nil && e.Customer.Id != 0 {
|
if e.Customer != nil && e.Customer.Id != 0 {
|
||||||
party.Name = e.Customer.Name
|
party.Name = e.Customer.Name
|
||||||
party.AccountNumber = e.Customer.AccountNumber
|
if party.AccountNumber == "" {
|
||||||
|
party.AccountNumber = e.Customer.AccountNumber
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case utils.PaymentPartySupplier:
|
case utils.PaymentPartySupplier:
|
||||||
if e.Supplier != nil && e.Supplier.Id != 0 {
|
if e.Supplier != nil && e.Supplier.Id != 0 {
|
||||||
party.Name = e.Supplier.Name
|
party.Name = e.Supplier.Name
|
||||||
if e.Supplier.AccountNumber != nil {
|
if party.AccountNumber == "" && e.Supplier.AccountNumber != nil {
|
||||||
party.AccountNumber = *e.Supplier.AccountNumber
|
party.AccountNumber = *e.Supplier.AccountNumber
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ func PaymentRoutes(v1 fiber.Router, u user.UserService, s payment.PaymentService
|
|||||||
route := v1.Group("/payments")
|
route := v1.Group("/payments")
|
||||||
route.Use(m.Auth(u))
|
route.Use(m.Auth(u))
|
||||||
|
|
||||||
route.Post("/",m.RequirePermissions(m.P_Finances_Payments_CreateOne), ctrl.CreateOne)
|
route.Post("/", m.RequirePermissions(m.P_Finances_Payments_CreateOne), ctrl.CreateOne)
|
||||||
route.Get("/:id",m.RequirePermissions(m.P_Finances_Payments_GetOne), ctrl.GetOne)
|
route.Get("/:id", m.RequirePermissions(m.P_Finances_Payments_GetOne), ctrl.GetOne)
|
||||||
route.Patch("/:id",m.RequirePermissions(m.P_Finances_Payments_UpdateOne), ctrl.UpdateOne)
|
route.Patch("/:id", m.RequirePermissions(m.P_Finances_Payments_UpdateOne), ctrl.UpdateOne)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,18 +121,19 @@ func (s *paymentService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
|||||||
}
|
}
|
||||||
|
|
||||||
createBody := &entity.Payment{
|
createBody := &entity.Payment{
|
||||||
PaymentCode: code,
|
PaymentCode: code,
|
||||||
ReferenceNumber: req.ReferenceNumber,
|
ReferenceNumber: req.ReferenceNumber,
|
||||||
TransactionType: transactionType,
|
TransactionType: transactionType,
|
||||||
PartyType: party,
|
PartyType: party,
|
||||||
PartyId: req.PartyId,
|
PartyId: req.PartyId,
|
||||||
PaymentDate: paymentDate,
|
PartyAccountNumber: req.PartyAccountNumber,
|
||||||
PaymentMethod: method,
|
PaymentDate: paymentDate,
|
||||||
BankId: req.BankId,
|
PaymentMethod: method,
|
||||||
Direction: directionForParty(party),
|
BankId: req.BankId,
|
||||||
Nominal: req.Nominal,
|
Direction: directionForParty(party),
|
||||||
Notes: req.Notes,
|
Nominal: req.Nominal,
|
||||||
CreatedBy: actorID,
|
Notes: req.Notes,
|
||||||
|
CreatedBy: actorID,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
@@ -188,6 +189,9 @@ func (s paymentService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
if req.ReferenceNumber != nil {
|
if req.ReferenceNumber != nil {
|
||||||
updateBody["reference_number"] = *req.ReferenceNumber
|
updateBody["reference_number"] = *req.ReferenceNumber
|
||||||
}
|
}
|
||||||
|
if req.PartyAccountNumber != nil {
|
||||||
|
updateBody["party_account_number"] = *req.PartyAccountNumber
|
||||||
|
}
|
||||||
if req.PaymentMethod != nil {
|
if req.PaymentMethod != nil {
|
||||||
method, err := normalizePaymentMethod(*req.PaymentMethod)
|
method, err := normalizePaymentMethod(*req.PaymentMethod)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
PartyType string `json:"party_type" validate:"required_strict,min=1,max=50"`
|
PartyType string `json:"party_type" validate:"required_strict,min=1,max=50"`
|
||||||
PartyId uint `json:"party_id" validate:"required_strict,number,gt=0"`
|
PartyId uint `json:"party_id" validate:"required_strict,number,gt=0"`
|
||||||
PaymentDate string `json:"payment_date" validate:"required_strict,datetime=2006-01-02"`
|
PartyAccountNumber *string `json:"party_account_number"`
|
||||||
Nominal float64 `json:"nominal" validate:"required_strict"`
|
PaymentDate string `json:"payment_date" validate:"required_strict,datetime=2006-01-02"`
|
||||||
ReferenceNumber *string `json:"reference_number,omitempty"`
|
Nominal float64 `json:"nominal" validate:"required_strict"`
|
||||||
PaymentMethod string `json:"payment_method" validate:"required_strict,max=20"`
|
ReferenceNumber *string `json:"reference_number,omitempty"`
|
||||||
BankId *uint `json:"bank_id" validate:"omitempty,number,gt=0"`
|
PaymentMethod string `json:"payment_method" validate:"required_strict,max=20"`
|
||||||
Notes string `json:"notes" validate:"required_strict,max=500"`
|
BankId *uint `json:"bank_id" validate:"omitempty,number,gt=0"`
|
||||||
|
Notes string `json:"notes" validate:"required_strict,max=500"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
PartyType *string `json:"party_type,omitempty" validate:"omitempty,max=50"`
|
PartyType *string `json:"party_type,omitempty" validate:"omitempty,max=50"`
|
||||||
PartyId *uint `json:"party_id,omitempty" validate:"omitempty,number,gt=0"`
|
PartyId *uint `json:"party_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||||
PaymentDate *string `json:"payment_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
|
PartyAccountNumber *string `json:"party_account_number,omitempty"`
|
||||||
Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"`
|
PaymentDate *string `json:"payment_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
|
||||||
ReferenceNumber *string `json:"reference_number,omitempty"`
|
Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"`
|
||||||
PaymentMethod *string `json:"payment_method,omitempty" validate:"omitempty,max=20"`
|
ReferenceNumber *string `json:"reference_number,omitempty"`
|
||||||
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
|
PaymentMethod *string `json:"payment_method,omitempty" validate:"omitempty,max=20"`
|
||||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||||
|
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
|
|||||||
@@ -124,20 +124,25 @@ func ToTransactionDetailDTO(e entity.Payment) TransactionDetailDTO {
|
|||||||
|
|
||||||
func partyFromPayment(e entity.Payment) Party {
|
func partyFromPayment(e entity.Payment) Party {
|
||||||
party := Party{
|
party := Party{
|
||||||
Id: e.PartyId,
|
Id: e.PartyId,
|
||||||
Type: e.PartyType,
|
Type: e.PartyType,
|
||||||
|
}
|
||||||
|
if e.PartyAccountNumber != nil {
|
||||||
|
party.AccountNumber = *e.PartyAccountNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
switch utils.PaymentParty(e.PartyType) {
|
switch utils.PaymentParty(e.PartyType) {
|
||||||
case utils.PaymentPartyCustomer:
|
case utils.PaymentPartyCustomer:
|
||||||
if e.Customer != nil && e.Customer.Id != 0 {
|
if e.Customer != nil && e.Customer.Id != 0 {
|
||||||
party.Name = e.Customer.Name
|
party.Name = e.Customer.Name
|
||||||
party.AccountNumber = e.Customer.AccountNumber
|
if party.AccountNumber == "" {
|
||||||
|
party.AccountNumber = e.Customer.AccountNumber
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case utils.PaymentPartySupplier:
|
case utils.PaymentPartySupplier:
|
||||||
if e.Supplier != nil && e.Supplier.Id != 0 {
|
if e.Supplier != nil && e.Supplier.Id != 0 {
|
||||||
party.Name = e.Supplier.Name
|
party.Name = e.Supplier.Name
|
||||||
if e.Supplier.AccountNumber != nil {
|
if party.AccountNumber == "" && e.Supplier.AccountNumber != nil {
|
||||||
party.AccountNumber = *e.Supplier.AccountNumber
|
party.AccountNumber = *e.Supplier.AccountNumber
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat
|
|||||||
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
||||||
|
|
||||||
err := fifoService.RegisterStockable(fifo.StockableConfig{
|
err := fifoService.RegisterStockable(fifo.StockableConfig{
|
||||||
Key: fifo.StockableKey("ADJUSTMENT_IN"),
|
Key: fifo.StockableKeyAdjustmentIn,
|
||||||
Table: "adjustment_stocks",
|
Table: "adjustment_stocks",
|
||||||
Columns: fifo.StockableColumns{
|
Columns: fifo.StockableColumns{
|
||||||
ID: "id",
|
ID: "id",
|
||||||
@@ -52,7 +52,7 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = fifoService.RegisterUsable(fifo.UsableConfig{
|
err = fifoService.RegisterUsable(fifo.UsableConfig{
|
||||||
Key: fifo.UsableKey("ADJUSTMENT_OUT"),
|
Key: fifo.UsableKeyAdjustmentOut,
|
||||||
Table: "adjustment_stocks",
|
Table: "adjustment_stocks",
|
||||||
Columns: fifo.UsableColumns{
|
Columns: fifo.UsableColumns{
|
||||||
ID: "id",
|
ID: "id",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -123,15 +124,9 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
projectFlockKandangID = &pfkID
|
projectFlockKandangID = &pfkID
|
||||||
}
|
}
|
||||||
|
|
||||||
pw, err := s.ProductWarehouseRepo.FindByProductWarehouseAndPfk(
|
pw, err := s.ProductWarehouseRepo.FindByProductWarehouseAndPfk(ctx, uint(req.ProductID), uint(req.WarehouseID), projectFlockKandangID)
|
||||||
ctx,
|
|
||||||
uint(req.ProductID),
|
|
||||||
uint(req.WarehouseID),
|
|
||||||
projectFlockKandangID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
s.Log.Errorf("Failed to find product warehouse: %+v", err)
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +138,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
|
if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
|
||||||
s.Log.Errorf("Failed to create product warehouse: %+v", err)
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create product warehouse")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create product warehouse")
|
||||||
}
|
}
|
||||||
pw = newPW
|
pw = newPW
|
||||||
@@ -163,7 +157,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create StockLog for history tracking
|
|
||||||
afterQuantity := productWarehouse.Quantity
|
afterQuantity := productWarehouse.Quantity
|
||||||
newLog := &entity.StockLog{
|
newLog := &entity.StockLog{
|
||||||
LoggableType: string(utils.StockLogTypeAdjustment),
|
LoggableType: string(utils.StockLogTypeAdjustment),
|
||||||
@@ -189,7 +182,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create AdjustmentStock record for FIFO tracking
|
|
||||||
adjustmentStock := &entity.AdjustmentStock{
|
adjustmentStock := &entity.AdjustmentStock{
|
||||||
StockLogId: newLog.Id,
|
StockLogId: newLog.Id,
|
||||||
ProductWarehouseId: productWarehouse.Id,
|
ProductWarehouseId: productWarehouse.Id,
|
||||||
@@ -200,10 +192,10 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
}
|
}
|
||||||
|
|
||||||
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
||||||
// Adjustment INCREASE → Replenish stock (Stockable)
|
|
||||||
note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id)
|
note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id)
|
||||||
_, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{
|
_, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{
|
||||||
StockableKey: "ADJUSTMENT_IN",
|
StockableKey: fifo.StockableKeyAdjustmentIn,
|
||||||
StockableID: adjustmentStock.Id,
|
StockableID: adjustmentStock.Id,
|
||||||
ProductWarehouseID: uint(productWarehouse.Id),
|
ProductWarehouseID: uint(productWarehouse.Id),
|
||||||
Quantity: req.Quantity,
|
Quantity: req.Quantity,
|
||||||
@@ -215,9 +207,8 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Adjustment DECREASE → Consume stock (Usable)
|
|
||||||
_, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{
|
_, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{
|
||||||
UsableKey: "ADJUSTMENT_OUT",
|
UsableKey: fifo.UsableKeyAdjustmentOut,
|
||||||
UsableID: adjustmentStock.Id,
|
UsableID: adjustmentStock.Id,
|
||||||
ProductWarehouseID: uint(productWarehouse.Id),
|
ProductWarehouseID: uint(productWarehouse.Id),
|
||||||
Quantity: req.Quantity,
|
Quantity: req.Quantity,
|
||||||
@@ -230,6 +221,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update ProductWarehouse quantity (for backward compatibility/reporting)
|
// Update ProductWarehouse quantity (for backward compatibility/reporting)
|
||||||
|
|
||||||
productWarehouse.Quantity = afterQuantity
|
productWarehouse.Quantity = afterQuantity
|
||||||
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil {
|
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil {
|
||||||
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
|
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
|
||||||
|
|||||||
+36
@@ -23,6 +23,7 @@ type ProductWarehouseRepository interface {
|
|||||||
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
|
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
|
||||||
GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error)
|
GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error)
|
||||||
GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error)
|
GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error)
|
||||||
|
ListProductIDsByFlagPrefixes(ctx context.Context, prefixes []string, visibleStatus *bool) ([]uint, error)
|
||||||
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
|
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
|
||||||
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
|
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
|
GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
|
||||||
@@ -380,3 +381,38 @@ func (r *ProductWarehouseRepositoryImpl) GetFirstProductByFlag(ctx context.Conte
|
|||||||
}
|
}
|
||||||
return &product, nil
|
return &product, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ProductWarehouseRepositoryImpl) ListProductIDsByFlagPrefixes(ctx context.Context, prefixes []string, visibleStatus *bool) ([]uint, error) {
|
||||||
|
if len(prefixes) == 0 {
|
||||||
|
return []uint{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Model(&entity.Product{}).
|
||||||
|
Distinct("products.id").
|
||||||
|
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", entity.FlagableTypeProduct)
|
||||||
|
|
||||||
|
applied := false
|
||||||
|
for _, prefix := range prefixes {
|
||||||
|
if prefix == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
like := prefix + "%"
|
||||||
|
if !applied {
|
||||||
|
db = db.Where("flags.name ILIKE ?", like)
|
||||||
|
applied = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
db = db.Or("flags.name ILIKE ?", like)
|
||||||
|
}
|
||||||
|
|
||||||
|
if visibleStatus != nil {
|
||||||
|
db = db.Where("products.is_visible = ?", *visibleStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids []uint
|
||||||
|
if err := db.Pluck("products.id", &ids).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ func (u *TransferController) GetOne(c *fiber.Ctx) error {
|
|||||||
func (u *TransferController) CreateOne(c *fiber.Ctx) error {
|
func (u *TransferController) CreateOne(c *fiber.Ctx) error {
|
||||||
data := c.FormValue("data")
|
data := c.FormValue("data")
|
||||||
|
|
||||||
|
const maxFileSize = 5 * 1024 * 1024
|
||||||
|
|
||||||
var req validation.TransferRequest
|
var req validation.TransferRequest
|
||||||
if err := json.Unmarshal([]byte(data), &req); err != nil {
|
if err := json.Unmarshal([]byte(data), &req); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
@@ -87,9 +89,11 @@ func (u *TransferController) CreateOne(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
files := form.File["documents"]
|
files := form.File["documents"]
|
||||||
|
|
||||||
if len(files) != len(req.Deliveries) {
|
for i, file := range files {
|
||||||
return fiber.NewError(fiber.StatusBadRequest,
|
if file.Size > maxFileSize {
|
||||||
fiber.NewError(fiber.StatusBadRequest, "Jumlah dokumen harus sama dengan jumlah deliveries").Message)
|
return fiber.NewError(fiber.StatusBadRequest,
|
||||||
|
"Dokumen ke-"+strconv.Itoa(i+1)+" melebihi ukuran maksimal 5MB")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.TransferService.CreateOne(c, &req, files)
|
result, err := u.TransferService.CreateOne(c, &req, files)
|
||||||
|
|||||||
@@ -71,9 +71,11 @@ type TransferDetailDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TransferDetailItemDTO struct {
|
type TransferDetailItemDTO struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
Product ProductSimpleDTO `json:"product"`
|
Product ProductSimpleDTO `json:"product"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
|
TransportPerItem *float64 `json:"transport_per_item,omitempty"` // Biaya ekspedisi per item
|
||||||
|
ExpeditionVendor *SupplierSimpleDTO `json:"expedition_vendor,omitempty"` // Vendor ekspedisi
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransferDeliveryDTO struct {
|
type TransferDeliveryDTO struct {
|
||||||
@@ -153,14 +155,30 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
|
|||||||
|
|
||||||
var details []TransferDetailItemDTO
|
var details []TransferDetailItemDTO
|
||||||
for _, d := range e.Details {
|
for _, d := range e.Details {
|
||||||
details = append(details, TransferDetailItemDTO{
|
detailDTO := TransferDetailItemDTO{
|
||||||
Id: d.Id,
|
Id: d.Id,
|
||||||
Product: ProductSimpleDTO{
|
Product: ProductSimpleDTO{
|
||||||
Id: d.Product.Id,
|
Id: d.Product.Id,
|
||||||
Name: d.Product.Name,
|
Name: d.Product.Name,
|
||||||
},
|
},
|
||||||
Quantity: d.UsageQty + d.PendingQty, // Total actual quantity allocated
|
Quantity: d.UsageQty + d.PendingQty, // Total actual quantity allocated
|
||||||
})
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if d.ExpenseNonstock != nil {
|
||||||
|
priceCopy := d.ExpenseNonstock.Price
|
||||||
|
detailDTO.TransportPerItem = &priceCopy
|
||||||
|
|
||||||
|
if d.ExpenseNonstock.Expense != nil && d.ExpenseNonstock.Expense.Supplier != nil && d.ExpenseNonstock.Expense.Supplier.Id != 0 {
|
||||||
|
exp := d.ExpenseNonstock.Expense
|
||||||
|
detailDTO.ExpeditionVendor = &SupplierSimpleDTO{
|
||||||
|
Id: exp.Supplier.Id,
|
||||||
|
Name: exp.Supplier.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
details = append(details, detailDTO)
|
||||||
}
|
}
|
||||||
|
|
||||||
var deliveries []TransferDeliveryDTO
|
var deliveries []TransferDeliveryDTO
|
||||||
@@ -223,18 +241,43 @@ func ToTransferListDTOs(e []entity.StockTransfer) []TransferListDTO {
|
|||||||
func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
|
func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
|
||||||
var details []TransferDetailItemDTO
|
var details []TransferDetailItemDTO
|
||||||
for _, d := range e.Details {
|
for _, d := range e.Details {
|
||||||
details = append(details, TransferDetailItemDTO{
|
detailDTO := TransferDetailItemDTO{
|
||||||
Id: d.Id,
|
Id: d.Id,
|
||||||
Product: ProductSimpleDTO{
|
Product: ProductSimpleDTO{
|
||||||
Id: d.Product.Id,
|
Id: d.Product.Id,
|
||||||
Name: d.Product.Name,
|
Name: d.Product.Name,
|
||||||
},
|
},
|
||||||
Quantity: d.UsageQty + d.PendingQty, // Total actual quantity allocated
|
Quantity: d.UsageQty + d.PendingQty, // Total actual quantity allocated
|
||||||
})
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if d.ExpenseNonstock != nil {
|
||||||
|
priceCopy := d.ExpenseNonstock.Price
|
||||||
|
detailDTO.TransportPerItem = &priceCopy
|
||||||
|
|
||||||
|
if d.ExpenseNonstock.Expense != nil && d.ExpenseNonstock.Expense.Supplier != nil && d.ExpenseNonstock.Expense.Supplier.Id != 0 {
|
||||||
|
exp := d.ExpenseNonstock.Expense
|
||||||
|
detailDTO.ExpeditionVendor = &SupplierSimpleDTO{
|
||||||
|
Id: exp.Supplier.Id,
|
||||||
|
Name: exp.Supplier.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
details = append(details, detailDTO)
|
||||||
}
|
}
|
||||||
|
|
||||||
var deliveries []TransferDeliveryDTO
|
var deliveries []TransferDeliveryDTO
|
||||||
for _, del := range e.Deliveries {
|
for _, del := range e.Deliveries {
|
||||||
|
var items []TransferDeliveryItemDTO
|
||||||
|
for _, item := range del.Items {
|
||||||
|
items = append(items, TransferDeliveryItemDTO{
|
||||||
|
Id: item.Id,
|
||||||
|
StockTransferDetailId: item.StockTransferDetailId,
|
||||||
|
Quantity: item.Quantity,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var document *DocumentDTO
|
var document *DocumentDTO
|
||||||
if len(del.Documents) > 0 {
|
if len(del.Documents) > 0 {
|
||||||
doc := del.Documents[0] // Take first document
|
doc := del.Documents[0] // Take first document
|
||||||
@@ -258,6 +301,7 @@ func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
|
|||||||
DocumentNumber: del.DocumentNumber,
|
DocumentNumber: del.DocumentNumber,
|
||||||
ShippingCostItem: del.ShippingCostItem,
|
ShippingCostItem: del.ShippingCostItem,
|
||||||
ShippingCostTotal: del.ShippingCostTotal,
|
ShippingCostTotal: del.ShippingCostTotal,
|
||||||
|
Items: items,
|
||||||
Document: document,
|
Document: document,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package transfers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -9,9 +10,13 @@ import (
|
|||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
|
expenseService "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
|
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
|
||||||
sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
|
sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
|
||||||
|
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
|
rNonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories"
|
||||||
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
||||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
@@ -35,20 +40,47 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||||
|
kandangRepo := rKandang.NewKandangRepository(db)
|
||||||
|
nonstockRepo := rNonstock.NewNonstockRepository(db)
|
||||||
documentRepo := commonRepo.NewDocumentRepository(db)
|
documentRepo := commonRepo.NewDocumentRepository(db)
|
||||||
stockAllocRepo := commonRepo.NewStockAllocationRepository(db)
|
stockAllocRepo := commonRepo.NewStockAllocationRepository(db)
|
||||||
|
expenseRepository := expenseRepo.NewExpenseRepository(db)
|
||||||
|
expenseRealizationRepo := expenseRepo.NewExpenseRealizationRepository(db)
|
||||||
|
|
||||||
documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo)
|
documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize FIFO Service
|
|
||||||
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
|
||||||
|
|
||||||
// Register Transfer as Stockable (adds stock to destination warehouse)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowExpense, utils.ExpenseApprovalSteps); err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to register expense approval workflow: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseServiceInstance := expenseService.NewExpenseService(
|
||||||
|
expenseRepository,
|
||||||
|
supplierRepo,
|
||||||
|
nonstockRepo,
|
||||||
|
approvalSvc,
|
||||||
|
expenseRealizationRepo,
|
||||||
|
projectFlockKandangRepo,
|
||||||
|
documentSvc,
|
||||||
|
validate,
|
||||||
|
)
|
||||||
|
|
||||||
|
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
||||||
|
expenseBridge := sTransfer.NewTransferExpenseBridge(
|
||||||
|
db,
|
||||||
|
stockTransferRepo,
|
||||||
|
projectFlockKandangRepo,
|
||||||
|
kandangRepo,
|
||||||
|
expenseServiceInstance,
|
||||||
|
)
|
||||||
|
|
||||||
err = fifoService.RegisterStockable(fifo.StockableConfig{
|
err = fifoService.RegisterStockable(fifo.StockableConfig{
|
||||||
Key: fifo.StockableKey("STOCK_TRANSFER_IN"),
|
Key: fifo.StockableKeyStockTransferIn,
|
||||||
Table: "stock_transfer_details",
|
Table: "stock_transfer_details",
|
||||||
Columns: fifo.StockableColumns{
|
Columns: fifo.StockableColumns{
|
||||||
ID: "id",
|
ID: "id",
|
||||||
@@ -63,9 +95,8 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register Transfer as Usable (consumes stock from source warehouse)
|
|
||||||
err = fifoService.RegisterUsable(fifo.UsableConfig{
|
err = fifoService.RegisterUsable(fifo.UsableConfig{
|
||||||
Key: fifo.UsableKey("STOCK_TRANSFER_OUT"),
|
Key: fifo.UsableKeyStockTransferOut,
|
||||||
Table: "stock_transfer_details",
|
Table: "stock_transfer_details",
|
||||||
Columns: fifo.UsableColumns{
|
Columns: fifo.UsableColumns{
|
||||||
ID: "id",
|
ID: "id",
|
||||||
@@ -80,7 +111,7 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo, documentSvc, fifoService)
|
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo, documentSvc, fifoService, expenseBridge)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
TransferRoutes(router, userService, transferService)
|
TransferRoutes(router, userService, transferService)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package repositories
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
@@ -12,6 +13,7 @@ type StockTransferRepository interface {
|
|||||||
repository.BaseRepository[entity.StockTransfer]
|
repository.BaseRepository[entity.StockTransfer]
|
||||||
// get sequence for movement number
|
// get sequence for movement number
|
||||||
GetNextMovementNumber(ctx context.Context) (int64, error)
|
GetNextMovementNumber(ctx context.Context) (int64, error)
|
||||||
|
GenerateMovementNumber(ctx context.Context) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type StockTransferRepositoryImpl struct {
|
type StockTransferRepositoryImpl struct {
|
||||||
@@ -32,3 +34,12 @@ func (r *StockTransferRepositoryImpl) GetNextMovementNumber(ctx context.Context)
|
|||||||
}
|
}
|
||||||
return seq, nil
|
return seq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *StockTransferRepositoryImpl) GenerateMovementNumber(ctx context.Context) (string, error) {
|
||||||
|
seq, err := r.GetNextMovementNumber(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
movementNumber := fmt.Sprintf("ST-%05d", seq)
|
||||||
|
return movementNumber, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -45,9 +46,10 @@ type transferService struct {
|
|||||||
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||||
DocumentSvc commonSvc.DocumentService
|
DocumentSvc commonSvc.DocumentService
|
||||||
FifoSvc commonSvc.FifoService
|
FifoSvc commonSvc.FifoService
|
||||||
|
ExpenseBridge TransferExpenseBridge
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService, fifoSvc commonSvc.FifoService) TransferService {
|
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService, fifoSvc commonSvc.FifoService, expenseBridge TransferExpenseBridge) TransferService {
|
||||||
return &transferService{
|
return &transferService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
@@ -62,6 +64,7 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr
|
|||||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
DocumentSvc: documentSvc,
|
DocumentSvc: documentSvc,
|
||||||
FifoSvc: fifoSvc,
|
FifoSvc: fifoSvc,
|
||||||
|
ExpenseBridge: expenseBridge,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +79,9 @@ func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
|
|||||||
Preload("ToWarehouse.Area").
|
Preload("ToWarehouse.Area").
|
||||||
Preload("Details").
|
Preload("Details").
|
||||||
Preload("Details.Product").
|
Preload("Details.Product").
|
||||||
|
Preload("Details.ExpenseNonstock").
|
||||||
|
Preload("Details.ExpenseNonstock.Expense").
|
||||||
|
Preload("Details.ExpenseNonstock.Expense.Supplier").
|
||||||
Preload("Deliveries.Items").
|
Preload("Deliveries.Items").
|
||||||
Preload("Deliveries.Supplier").
|
Preload("Deliveries.Supplier").
|
||||||
Preload("Deliveries.Documents", func(db *gorm.DB) *gorm.DB {
|
Preload("Deliveries.Documents", func(db *gorm.DB) *gorm.DB {
|
||||||
@@ -93,7 +99,7 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
|
|||||||
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
if params.Search != "" {
|
if params.Search != "" {
|
||||||
db = db.Where("movement_number LIKE ?", "%"+strings.TrimSpace(params.Search)+"%")
|
db = db.Where("movement_number ILIKE ?", "%"+strings.TrimSpace(params.Search)+"%")
|
||||||
}
|
}
|
||||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
})
|
})
|
||||||
@@ -122,7 +128,6 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
|
|||||||
|
|
||||||
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest, files []*multipart.FileHeader) (*entity.StockTransfer, error) {
|
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest, files []*multipart.FileHeader) (*entity.StockTransfer, error) {
|
||||||
|
|
||||||
// === VALIDASI SOURCE WAREHOUSE ===
|
|
||||||
pwIDs := make([]uint, 0, len(req.Products))
|
pwIDs := make([]uint, 0, len(req.Products))
|
||||||
|
|
||||||
for _, product := range req.Products {
|
for _, product := range req.Products {
|
||||||
@@ -154,14 +159,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.ProjectFlockKandangRepo != nil {
|
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
|
||||||
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
|
if err != nil {
|
||||||
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")
|
}
|
||||||
}
|
if projectFlockKandang.ClosedAt != nil {
|
||||||
if projectFlockKandang.ClosedAt != nil {
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock tujuan sudah closing")
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock tujuan sudah closing")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID, err := m.ActorIDFromContext(c)
|
actorID, err := m.ActorIDFromContext(c)
|
||||||
@@ -191,16 +194,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
}
|
}
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek data supplier")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek data supplier")
|
||||||
}
|
}
|
||||||
if supplier.Category != "BOP" {
|
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 dengan ID %d bukan kategori BOP", delivery.SupplierID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
seqNum, err := s.StockTransferRepo.GetNextMovementNumber(c.Context())
|
movementNumber, err := s.StockTransferRepo.GenerateMovementNumber(c.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number")
|
||||||
}
|
}
|
||||||
movementNumber := fmt.Sprintf("PND-MBU-%05d", seqNum)
|
|
||||||
transferDate, _ := utils.ParseDateString(req.TransferDate)
|
transferDate, _ := utils.ParseDateString(req.TransferDate)
|
||||||
|
|
||||||
entityTransfer := &entity.StockTransfer{
|
entityTransfer := &entity.StockTransfer{
|
||||||
@@ -212,19 +215,26 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
CreatedBy: uint64(actorID),
|
CreatedBy: uint64(actorID),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expensePayloads := make([]TransferExpenseReceivingPayload, 0)
|
||||||
|
|
||||||
err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
|
|
||||||
if err := s.StockTransferRepo.WithTx(tx).CreateOne(c.Context(), entityTransfer, nil); err != nil {
|
stockTransferRepoTX := s.StockTransferRepo.WithTx(tx)
|
||||||
|
stockTransferDetailRepoTX := s.StockTransferDetailRepo.WithTx(tx)
|
||||||
|
stockTransferDeliveryRepoTX := s.StockTransferDeliveryRepo.WithTx(tx)
|
||||||
|
stockTransferDeliveryItemRepoTX := s.StockTransferDeliveryItemRepo.WithTx(tx)
|
||||||
|
productWarehouseRepoTX := rProductWarehouse.NewProductWarehouseRepository(tx)
|
||||||
|
|
||||||
|
if err := stockTransferRepoTX.CreateOne(c.Context(), entityTransfer, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare details and fetch product warehouses
|
|
||||||
details := make([]*entity.StockTransferDetail, 0, len(req.Products))
|
details := make([]*entity.StockTransferDetail, 0, len(req.Products))
|
||||||
detailMap := make(map[uint64]*entity.StockTransferDetail)
|
detailMap := make(map[uint64]*entity.StockTransferDetail)
|
||||||
|
|
||||||
for _, product := range req.Products {
|
for _, product := range req.Products {
|
||||||
// Get source product warehouse
|
|
||||||
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
sourcePW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID(
|
||||||
c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID),
|
c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -234,8 +244,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse source")
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse source")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get or create destination product warehouse
|
destPW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID(
|
||||||
destPW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
|
||||||
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
|
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
|
||||||
)
|
)
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -253,7 +262,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
Quantity: 0,
|
Quantity: 0,
|
||||||
ProjectFlockKandangId: &projectFlockKandangID,
|
ProjectFlockKandangId: &projectFlockKandangID,
|
||||||
}
|
}
|
||||||
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
|
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, "Gagal membuat product warehouse destination")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,7 +283,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
detailMap[uint64(product.ProductID)] = detail
|
detailMap[uint64(product.ProductID)] = detail
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.StockTransferDetailRepo.WithTx(tx).CreateMany(c.Context(), details, nil); err != nil {
|
if err := stockTransferDetailRepoTX.CreateMany(c.Context(), details, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,7 +298,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
ShippingCostTotal: delivery.DeliveryCost,
|
ShippingCostTotal: delivery.DeliveryCost,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err := s.StockTransferDeliveryRepo.WithTx(tx).CreateMany(c.Context(), deliveries, nil); err != nil {
|
if err := stockTransferDeliveryRepoTX.CreateMany(c.Context(), deliveries, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,52 +318,63 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := s.StockTransferDeliveryItemRepo.WithTx(tx).CreateMany(c.Context(), deliveryItems, nil); err != nil {
|
if err := stockTransferDeliveryItemRepoTX.CreateMany(c.Context(), deliveryItems, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.DocumentSvc != nil && len(files) > 0 {
|
if s.DocumentSvc != nil && len(files) > 0 {
|
||||||
|
|
||||||
for idx, file := range files {
|
for deliveryIdx, delivery := range deliveries {
|
||||||
|
reqDelivery := req.Deliveries[deliveryIdx]
|
||||||
|
|
||||||
|
if reqDelivery.DocumentIndex < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if reqDelivery.DocumentIndex >= len(files) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("DocumentIndex %d untuk delivery %d melebihi jumlah file yang diupload (%d)",
|
||||||
|
reqDelivery.DocumentIndex, deliveryIdx+1, len(files)))
|
||||||
|
}
|
||||||
|
|
||||||
|
file := files[reqDelivery.DocumentIndex]
|
||||||
|
|
||||||
documentFiles := []commonSvc.DocumentFile{
|
documentFiles := []commonSvc.DocumentFile{
|
||||||
{
|
{
|
||||||
File: file,
|
File: file,
|
||||||
Type: string(utils.DocumentTypeTransfer),
|
Type: string(utils.DocumentTypeTransfer),
|
||||||
Index: &idx,
|
Index: &reqDelivery.DocumentIndex,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
|
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
|
||||||
DocumentableType: string(utils.DocumentableTypeTransfer),
|
DocumentableType: string(utils.DocumentableTypeTransfer),
|
||||||
DocumentableID: deliveries[idx].Id,
|
DocumentableID: delivery.Id,
|
||||||
CreatedBy: &actorID,
|
CreatedBy: &actorID,
|
||||||
Files: documentFiles,
|
Files: documentFiles,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.WithError(err).Errorf("Failed to upload document for delivery %d (delivery_id: %d, filename: %s)",
|
s.Log.WithError(err).Errorf("Failed to upload document for delivery %d (delivery_id: %d, filename: %s)",
|
||||||
idx+1, deliveries[idx].Id, file.Filename)
|
deliveryIdx+1, delivery.Id, file.Filename)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to upload document for delivery %d: %v", idx+1, err))
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to upload document for delivery %d: %v", deliveryIdx+1, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute FIFO operations for each product
|
|
||||||
for _, product := range req.Products {
|
for _, product := range req.Products {
|
||||||
detail := detailMap[uint64(product.ProductID)]
|
detail := detailMap[uint64(product.ProductID)]
|
||||||
|
|
||||||
// Step 1: Consume stock from source warehouse (STOCK_TRANSFER_OUT)
|
|
||||||
consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
|
consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
|
||||||
UsableKey: "STOCK_TRANSFER_OUT",
|
UsableKey: fifo.UsableKeyStockTransferOut,
|
||||||
UsableID: uint(detail.Id),
|
UsableID: uint(detail.Id),
|
||||||
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
|
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
|
||||||
Quantity: product.ProductQty,
|
Quantity: product.ProductQty,
|
||||||
AllowPending: false, // Don't allow pending, must have actual stock
|
AllowPending: false,
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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 cukup di gudang asal untuk produk %d: %v", product.ProductID, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update usage tracking fields for source warehouse
|
|
||||||
if err := tx.Model(&entity.StockTransferDetail{}).
|
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||||
Where("id = ?", detail.Id).
|
Where("id = ?", detail.Id).
|
||||||
Updates(map[string]interface{}{
|
Updates(map[string]interface{}{
|
||||||
@@ -364,10 +384,9 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
return fmt.Errorf("gagal update usage tracking: %w", err)
|
return fmt.Errorf("gagal update usage tracking: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Replenish stock to destination warehouse (STOCK_TRANSFER_IN)
|
|
||||||
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
|
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
|
||||||
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
||||||
StockableKey: "STOCK_TRANSFER_IN",
|
StockableKey: fifo.StockableKeyStockTransferIn,
|
||||||
StockableID: uint(detail.Id),
|
StockableID: uint(detail.Id),
|
||||||
ProductWarehouseID: uint(*detail.DestProductWarehouseID),
|
ProductWarehouseID: uint(*detail.DestProductWarehouseID),
|
||||||
Quantity: product.ProductQty,
|
Quantity: product.ProductQty,
|
||||||
@@ -378,7 +397,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
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 di gudang tujuan untuk produk %d: %v", product.ProductID, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update total tracking fields for destination warehouse
|
|
||||||
if err := tx.Model(&entity.StockTransferDetail{}).
|
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||||
Where("id = ?", detail.Id).
|
Where("id = ?", detail.Id).
|
||||||
Updates(map[string]interface{}{
|
Updates(map[string]interface{}{
|
||||||
@@ -388,6 +406,32 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(req.Deliveries) > 0 {
|
||||||
|
for _, delivery := range req.Deliveries {
|
||||||
|
for _, prod := range delivery.Products {
|
||||||
|
detail := detailMap[uint64(prod.ProductID)]
|
||||||
|
if detail == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
warehouseID := uint(req.DestinationWarehouseID)
|
||||||
|
supplierID := uint(delivery.SupplierID)
|
||||||
|
deliveredDate := transferDate
|
||||||
|
deliveredQty := prod.ProductQty
|
||||||
|
|
||||||
|
payload := TransferExpenseReceivingPayload{
|
||||||
|
TransferDetailID: detail.Id,
|
||||||
|
ProductID: uint64(prod.ProductID),
|
||||||
|
WarehouseID: uint64(warehouseID),
|
||||||
|
SupplierID: uint64(supplierID),
|
||||||
|
DeliveredQty: deliveredQty,
|
||||||
|
DeliveredDate: &deliveredDate,
|
||||||
|
}
|
||||||
|
expensePayloads = append(expensePayloads, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -399,9 +443,31 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID uint64, payloads []TransferExpenseReceivingPayload) error {
|
||||||
|
if s.ExpenseBridge == nil || transferID == 0 || len(payloads) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
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) {
|
func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
|
||||||
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
|
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,473 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
expenseDto "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto"
|
||||||
|
expenseSvc "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
||||||
|
expenseValidation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations"
|
||||||
|
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
|
||||||
|
kandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TransferExpenseBridge interface {
|
||||||
|
OnItemsDeleted(ctx context.Context, transferID uint64, items []entity.StockTransferDetail) error
|
||||||
|
OnItemsDelivered(c *fiber.Ctx, transferID uint64, updates []TransferExpenseReceivingPayload) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransferExpenseReceivingPayload struct {
|
||||||
|
TransferDetailID uint64
|
||||||
|
ProductID uint64
|
||||||
|
WarehouseID uint64
|
||||||
|
SupplierID uint64
|
||||||
|
TransportPerItem *float64
|
||||||
|
DeliveredQty float64
|
||||||
|
DeliveredDate *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type groupedTransferItem struct {
|
||||||
|
detail *entity.StockTransferDetail
|
||||||
|
payload TransferExpenseReceivingPayload
|
||||||
|
projectFK *uint
|
||||||
|
kandangID *uint
|
||||||
|
totalPrice float64
|
||||||
|
shippingCostTotal float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupingKey(supplierID uint, date time.Time, warehouseID uint) string {
|
||||||
|
return fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(date), warehouseID)
|
||||||
|
}
|
||||||
|
|
||||||
|
type transferExpenseBridge struct {
|
||||||
|
db *gorm.DB
|
||||||
|
transferRepo rStockTransfer.StockTransferRepository
|
||||||
|
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||||
|
kandangRepo kandangRepo.KandangRepository
|
||||||
|
expenseSvc expenseSvc.ExpenseService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTransferExpenseBridge(
|
||||||
|
db *gorm.DB,
|
||||||
|
transferRepo rStockTransfer.StockTransferRepository,
|
||||||
|
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
|
||||||
|
kandangRepo kandangRepo.KandangRepository,
|
||||||
|
expenseSvc expenseSvc.ExpenseService,
|
||||||
|
) TransferExpenseBridge {
|
||||||
|
return &transferExpenseBridge{
|
||||||
|
db: db,
|
||||||
|
transferRepo: transferRepo,
|
||||||
|
projectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
|
kandangRepo: kandangRepo,
|
||||||
|
expenseSvc: expenseSvc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, items []entity.StockTransferDetail) error {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(expenseNonstockIDs) > 0 {
|
||||||
|
|
||||||
|
for _, nsID := range expenseNonstockIDs {
|
||||||
|
var expenseID uint64
|
||||||
|
if err := tx.Model(&entity.ExpenseNonstock{}).
|
||||||
|
Select("expense_id").
|
||||||
|
Where("id = ?", nsID).
|
||||||
|
Scan(&expenseID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if expenseID != 0 {
|
||||||
|
expenseIDs[expenseID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if err := tx.Delete(&entity.ExpenseNonstock{}, expenseNonstockIDs).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
approvalRepoTx := commonRepo.NewApprovalRepository(tx)
|
||||||
|
for expenseID := range expenseIDs {
|
||||||
|
var count int64
|
||||||
|
if err := tx.Model(&entity.ExpenseNonstock{}).
|
||||||
|
Where("expense_id = ?", expenseID).
|
||||||
|
Count(&count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(expenseID)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Delete(&entity.Expense{}, expenseID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *transferExpenseBridge) markExpensesUpdated(ctx context.Context, expenseIDs map[uint64]struct{}, actorID uint) error {
|
||||||
|
if len(expenseIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if actorID == 0 {
|
||||||
|
actorID = 1
|
||||||
|
}
|
||||||
|
svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
||||||
|
action := entity.ApprovalActionUpdated
|
||||||
|
for id := range expenseIDs {
|
||||||
|
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *transferExpenseBridge) findExpeditionNonstockID(ctx context.Context, supplierID uint) (uint64, error) {
|
||||||
|
var id uint64
|
||||||
|
err := b.db.WithContext(ctx).
|
||||||
|
Table("nonstocks AS ns").
|
||||||
|
Select("ns.id").
|
||||||
|
Joins("JOIN nonstock_suppliers nss ON nss.nonstock_id = ns.id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = ns.id AND f.flagable_type = ?", entity.FlagableTypeNonstock).
|
||||||
|
Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi))).
|
||||||
|
Where("nss.supplier_id = ?", supplierID).
|
||||||
|
Order("ns.id").
|
||||||
|
Limit(1).
|
||||||
|
Scan(&id).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if id == 0 {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, "supplier id tidak sesuai dengan expedisi")
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *transferExpenseBridge) createExpenseViaService(
|
||||||
|
c *fiber.Ctx,
|
||||||
|
transfer *entity.StockTransfer,
|
||||||
|
items []groupedTransferItem,
|
||||||
|
expenseDate time.Time,
|
||||||
|
expeditionNonstockID uint64,
|
||||||
|
movementNumber string,
|
||||||
|
supplierID uint,
|
||||||
|
) (*expenseDto.ExpenseDetailDTO, error) {
|
||||||
|
ctx := c.Context()
|
||||||
|
if b.expenseSvc == nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "expense service not available")
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "no items to create expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangID := items[0].kandangID
|
||||||
|
var locationID uint64
|
||||||
|
var expenseKandangID *uint64
|
||||||
|
if kandangID != nil && *kandangID != 0 {
|
||||||
|
kandang, err := b.kandangRepo.GetByID(ctx, *kandangID, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Select("id, location_id")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang not found: %d", *kandangID))
|
||||||
|
}
|
||||||
|
if kandang == nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang not found: %d", *kandangID))
|
||||||
|
}
|
||||||
|
locationID = uint64(kandang.LocationId)
|
||||||
|
id := uint64(*kandangID)
|
||||||
|
expenseKandangID = &id
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if transfer.ToWarehouse == nil || transfer.ToWarehouse.LocationId == nil || *transfer.ToWarehouse.LocationId == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Destination warehouse location is required for expense")
|
||||||
|
}
|
||||||
|
locationID = uint64(*transfer.ToWarehouse.LocationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
costItems := make([]expenseValidation.CostItem, 0, len(items))
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
costItems = append(costItems, expenseValidation.CostItem{
|
||||||
|
NonstockID: expeditionNonstockID,
|
||||||
|
Quantity: 1,
|
||||||
|
Price: price,
|
||||||
|
Notes: note,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &expenseValidation.Create{
|
||||||
|
PoNumber: "",
|
||||||
|
TransactionDate: utils.FormatDate(expenseDate),
|
||||||
|
Category: string(utils.ExpenseCategoryBOP),
|
||||||
|
SupplierID: uint64(supplierID),
|
||||||
|
LocationID: locationID,
|
||||||
|
ExpenseNonstocks: []expenseValidation.ExpenseNonstock{{
|
||||||
|
KandangID: expenseKandangID,
|
||||||
|
CostItems: costItems,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
detail, err := b.expenseSvc.CreateOne(c, req)
|
||||||
|
if err != nil {
|
||||||
|
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 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return detail, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *transferExpenseBridge) linkExpenseNonstocksToDetails(ctx context.Context, detail *expenseDto.ExpenseDetailDTO, items []groupedTransferItem) error {
|
||||||
|
if detail == nil || len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
noteToExpenseNonstock := mapExpenseNotesForTransfer(detail)
|
||||||
|
|
||||||
|
if len(noteToExpenseNonstock) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, gi := range items {
|
||||||
|
expenseNonstockID, ok := noteToExpenseNonstock[gi.detail.Id]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := b.db.WithContext(ctx).
|
||||||
|
Model(&entity.StockTransferDetail{}).
|
||||||
|
Where("id = ?", gi.detail.Id).
|
||||||
|
Update("expense_nonstock_id", expenseNonstockID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapExpenseNotesForTransfer(detail *expenseDto.ExpenseDetailDTO) map[uint64]uint64 {
|
||||||
|
result := make(map[uint64]uint64)
|
||||||
|
if detail == nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
for _, kandang := range detail.Kandangs {
|
||||||
|
for _, pengajuan := range kandang.Pengajuans {
|
||||||
|
note := strings.TrimSpace(pengajuan.Notes)
|
||||||
|
if note == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const prefix = "stock_transfer_detail:"
|
||||||
|
if !strings.HasPrefix(note, prefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
idStr := strings.TrimPrefix(note, prefix)
|
||||||
|
var detailID uint64
|
||||||
|
if _, err := fmt.Sscanf(idStr, "%d", &detailID); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[detailID] = pengajuan.Id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64, updates []TransferExpenseReceivingPayload) error {
|
||||||
|
if transferID == 0 || len(updates) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := c.Context()
|
||||||
|
|
||||||
|
|
||||||
|
transfer, err := b.transferRepo.GetByID(ctx, uint(transferID), func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.
|
||||||
|
Preload("Details").
|
||||||
|
Preload("Details.Product").
|
||||||
|
Preload("Details.DestProductWarehouse").
|
||||||
|
Preload("Details.DeliveryItems").
|
||||||
|
Preload("Details.DeliveryItems.StockTransferDelivery").
|
||||||
|
Preload("ToWarehouse")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
detailMap := make(map[uint64]*entity.StockTransferDetail, len(transfer.Details))
|
||||||
|
shippingCostMap := make(map[uint64]float64) // detailID -> ShippingCostTotal
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := make(map[string][]groupedTransferItem)
|
||||||
|
|
||||||
|
for _, payload := range updates {
|
||||||
|
if payload.DeliveredDate == nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "delivered_date is required")
|
||||||
|
}
|
||||||
|
detail := detailMap[payload.TransferDetailID]
|
||||||
|
if detail == nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Transfer detail %d not found", payload.TransferDetailID))
|
||||||
|
}
|
||||||
|
if payload.DeliveredQty <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Delivered quantity for detail %d must be greater than 0", payload.TransferDetailID))
|
||||||
|
}
|
||||||
|
|
||||||
|
deliveredDate := payload.DeliveredDate.UTC().Truncate(24 * time.Hour)
|
||||||
|
supplierID := payload.SupplierID
|
||||||
|
if supplierID == 0 {
|
||||||
|
supplierID = 1 // Default supplier
|
||||||
|
}
|
||||||
|
|
||||||
|
var kandangID *uint
|
||||||
|
var projectFK *uint
|
||||||
|
if detail.DestProductWarehouse.WarehouseId != 0 {
|
||||||
|
var kandangIDResult uint
|
||||||
|
if err := b.db.WithContext(ctx).
|
||||||
|
Table("warehouses").
|
||||||
|
Select("kandang_id").
|
||||||
|
Where("id = ?", detail.DestProductWarehouse.WarehouseId).
|
||||||
|
Scan(&kandangIDResult).Error; err == nil && kandangIDResult != 0 {
|
||||||
|
id := uint(kandangIDResult)
|
||||||
|
kandangID = &id
|
||||||
|
if project, err := b.projectFlockKandangRepo.GetActiveByKandangID(ctx, kandangIDResult); err == nil && project != nil {
|
||||||
|
pid := uint(project.Id)
|
||||||
|
projectFK = &pid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if warehouseID == 0 && detail.DestProductWarehouse != nil {
|
||||||
|
warehouseID = uint(detail.DestProductWarehouse.WarehouseId)
|
||||||
|
}
|
||||||
|
|
||||||
|
key := groupingKey(uint(supplierID), deliveredDate, warehouseID)
|
||||||
|
groups[key] = append(groups[key], groupedTransferItem{
|
||||||
|
detail: detail,
|
||||||
|
payload: payload,
|
||||||
|
projectFK: projectFK,
|
||||||
|
kandangID: kandangID,
|
||||||
|
totalPrice: totalPrice,
|
||||||
|
shippingCostTotal: shippingCostTotal,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedExpenses := make(map[uint64]struct{})
|
||||||
|
|
||||||
|
for key, items := range groups {
|
||||||
|
if len(items) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.Split(key, ":")
|
||||||
|
if len(parts) < 3 {
|
||||||
|
return errors.New("invalid expense grouping key")
|
||||||
|
}
|
||||||
|
expenseDate, err := utils.ParseDateString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
supplierID, err := strconv.ParseUint(parts[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
expeditionNonstockID, err := b.findExpeditionNonstockID(ctx, uint(supplierID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseDetail, err := b.createExpenseViaService(c, transfer, items, expenseDate, expeditionNonstockID, transfer.MovementNumber, uint(supplierID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := b.linkExpenseNonstocksToDetails(ctx, expenseDetail, items); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if expenseDetail != nil && expenseDetail.Id != 0 {
|
||||||
|
updatedExpenses[uint64(expenseDetail.Id)] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updatedExpenses) > 0 {
|
||||||
|
actorID := uint(1) // Default actor
|
||||||
|
if err := b.markExpensesUpdated(ctx, updatedExpenses, actorID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ type TransferDeliveryProduct struct {
|
|||||||
type TransferDelivery struct {
|
type TransferDelivery struct {
|
||||||
DeliveryCost float64 `json:"delivery_cost" validate:"required"`
|
DeliveryCost float64 `json:"delivery_cost" validate:"required"`
|
||||||
DeliveryCostPerItem float64 `json:"delivery_cost_per_item" validate:"required"`
|
DeliveryCostPerItem float64 `json:"delivery_cost_per_item" validate:"required"`
|
||||||
DocumentIndex int `json:"document_index" validate:"min=0"`
|
DocumentIndex int `json:"document_index" validate:"omitempty,min=-1" default:"-1"`
|
||||||
DriverName string `json:"driver_name" validate:"required"`
|
DriverName string `json:"driver_name" validate:"required"`
|
||||||
VehiclePlate string `json:"vehicle_plate" validate:"required"`
|
VehiclePlate string `json:"vehicle_plate" validate:"required"`
|
||||||
SupplierID uint `json:"supplier_id" validate:"required"`
|
SupplierID uint `json:"supplier_id" validate:"required"`
|
||||||
|
|||||||
@@ -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 {
|
areas, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
if params.Search != "" {
|
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")
|
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 {
|
banks, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
if params.Search != "" {
|
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")
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/dto"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/services"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigChecklistController struct {
|
||||||
|
ConfigChecklistService service.ConfigChecklistService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigChecklistController(configChecklistService service.ConfigChecklistService) *ConfigChecklistController {
|
||||||
|
return &ConfigChecklistController{
|
||||||
|
ConfigChecklistService: configChecklistService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ConfigChecklistController) GetAll(c *fiber.Ctx) error {
|
||||||
|
query := &validation.Query{
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
Search: c.Query("search", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, totalResults, err := u.ConfigChecklistService.GetAll(c, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.SuccessWithPaginate[dto.ConfigChecklistListDTO]{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get all configChecklists successfully",
|
||||||
|
Meta: response.Meta{
|
||||||
|
Page: query.Page,
|
||||||
|
Limit: query.Limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
TotalResults: totalResults,
|
||||||
|
},
|
||||||
|
Data: dto.ToConfigChecklistListDTOs(result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ConfigChecklistController) GetOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ConfigChecklistService.GetOne(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get configChecklist successfully",
|
||||||
|
Data: dto.ToConfigChecklistListDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ConfigChecklistController) CreateOne(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Create)
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ConfigChecklistService.CreateOne(c, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusCreated,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Create configChecklist successfully",
|
||||||
|
Data: dto.ToConfigChecklistListDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ConfigChecklistController) UpdateOne(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Update)
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ConfigChecklistService.UpdateOne(c, req, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Update configChecklist successfully",
|
||||||
|
Data: dto.ToConfigChecklistListDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ConfigChecklistController) DeleteOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.ConfigChecklistService.DeleteOne(c, uint(id)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Common{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Delete configChecklist successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === DTO Structs ===
|
||||||
|
|
||||||
|
type ConfigChecklistRelationDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigChecklistListDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
PercentageThresholdBad int `json:"percentage_threshold_bad"`
|
||||||
|
PercentageThresholdEnough int `json:"percentage_threshold_enough"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigChecklistDetailDTO struct {
|
||||||
|
ConfigChecklistListDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions ===
|
||||||
|
|
||||||
|
func ToConfigChecklistRelationDTO(e entity.ConfigChecklist) ConfigChecklistRelationDTO {
|
||||||
|
return ConfigChecklistRelationDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
Date: e.Date,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToConfigChecklistListDTO(e entity.ConfigChecklist) ConfigChecklistListDTO {
|
||||||
|
return ConfigChecklistListDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
Date: e.Date,
|
||||||
|
PercentageThresholdBad: e.PercentageThresholdBad,
|
||||||
|
PercentageThresholdEnough: e.PercentageThresholdEnough,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToConfigChecklistListDTOs(e []entity.ConfigChecklist) []ConfigChecklistListDTO {
|
||||||
|
result := make([]ConfigChecklistListDTO, len(e))
|
||||||
|
for i, r := range e {
|
||||||
|
result[i] = ToConfigChecklistListDTO(r)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToConfigChecklistDetailDTO(e entity.ConfigChecklist) ConfigChecklistDetailDTO {
|
||||||
|
return ConfigChecklistDetailDTO{
|
||||||
|
ConfigChecklistListDTO: ToConfigChecklistListDTO(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package configChecklists
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
rConfigChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/repositories"
|
||||||
|
sConfigChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/services"
|
||||||
|
|
||||||
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigChecklistModule struct{}
|
||||||
|
|
||||||
|
func (ConfigChecklistModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
configChecklistRepo := rConfigChecklist.NewConfigChecklistRepository(db)
|
||||||
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
|
configChecklistService := sConfigChecklist.NewConfigChecklistService(configChecklistRepo, validate)
|
||||||
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
ConfigChecklistRoutes(router, userService, configChecklistService)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigChecklistRepository interface {
|
||||||
|
repository.BaseRepository[entity.ConfigChecklist]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigChecklistRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[entity.ConfigChecklist]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigChecklistRepository(db *gorm.DB) ConfigChecklistRepository {
|
||||||
|
return &ConfigChecklistRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.ConfigChecklist](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package configChecklists
|
||||||
|
|
||||||
|
import (
|
||||||
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/controllers"
|
||||||
|
configChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/services"
|
||||||
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ConfigChecklistRoutes(v1 fiber.Router, u user.UserService, s configChecklist.ConfigChecklistService) {
|
||||||
|
ctrl := controller.NewConfigChecklistController(s)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/repositories"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigChecklistService interface {
|
||||||
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ConfigChecklist, int64, error)
|
||||||
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.ConfigChecklist, error)
|
||||||
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ConfigChecklist, error)
|
||||||
|
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ConfigChecklist, error)
|
||||||
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type configChecklistService struct {
|
||||||
|
Log *logrus.Logger
|
||||||
|
Validate *validator.Validate
|
||||||
|
Repository repository.ConfigChecklistRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigChecklistService(repo repository.ConfigChecklistRepository, validate *validator.Validate) ConfigChecklistService {
|
||||||
|
return &configChecklistService{
|
||||||
|
Log: utils.Log,
|
||||||
|
Validate: validate,
|
||||||
|
Repository: repo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s configChecklistService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s configChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ConfigChecklist, int64, error) {
|
||||||
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
|
configChecklists, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
|
db = s.withRelations(db)
|
||||||
|
return db.Order("date DESC").Order("created_at DESC")
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to get configChecklists: %+v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return configChecklists, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s configChecklistService) GetOne(c *fiber.Ctx, id uint) (*entity.ConfigChecklist, error) {
|
||||||
|
configChecklist, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "ConfigChecklist not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed get configChecklist by id: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return configChecklist, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *configChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ConfigChecklist, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
date, err := time.Parse("2006-01-02", req.Date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid date format, use YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
|
||||||
|
createBody := &entity.ConfigChecklist{
|
||||||
|
Date: date,
|
||||||
|
PercentageThresholdBad: req.PercentageThresholdBad,
|
||||||
|
PercentageThresholdEnough: req.PercentageThresholdEnough,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
|
||||||
|
s.Log.Errorf("Failed to create configChecklist: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetOne(c, createBody.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s configChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ConfigChecklist, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBody := make(map[string]any)
|
||||||
|
|
||||||
|
if req.Date != nil {
|
||||||
|
date, err := time.Parse("2006-01-02", *req.Date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid date format, use YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
updateBody["date"] = date
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PercentageThresholdBad != nil {
|
||||||
|
updateBody["percentage_threshold_bad"] = *req.PercentageThresholdBad
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PercentageThresholdEnough != nil {
|
||||||
|
updateBody["percentage_threshold_enough"] = *req.PercentageThresholdEnough
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updateBody) == 0 {
|
||||||
|
return s.GetOne(c, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "ConfigChecklist not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to update configChecklist: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetOne(c, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s configChecklistService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||||
|
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "ConfigChecklist not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to delete configChecklist: %+v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
type Create struct {
|
||||||
|
Date string `json:"date" validate:"required"`
|
||||||
|
PercentageThresholdBad int `json:"percentage_threshold_bad" validate:"required"`
|
||||||
|
PercentageThresholdEnough int `json:"percentage_threshold_enough" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Update struct {
|
||||||
|
Date *string `json:"date,omitempty" validate:"omitempty"`
|
||||||
|
PercentageThresholdBad *int `json:"percentage_threshold_bad,omitempty" validate:"omitempty"`
|
||||||
|
PercentageThresholdEnough *int `json:"percentage_threshold_enough,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"`
|
||||||
|
}
|
||||||
@@ -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 {
|
customers, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
if params.Search != "" {
|
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")
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
|||||||
employeess, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
employeess, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
if params.Search != "" {
|
if params.Search != "" {
|
||||||
db = db.Where("employees.name LIKE ?", "%"+params.Search+"%")
|
db = db.Where("employees.name ILIKE ?", "%"+params.Search+"%")
|
||||||
}
|
}
|
||||||
if params.KandangId != nil {
|
if params.KandangId != nil {
|
||||||
db = db.Joins("JOIN employee_kandangs ek ON ek.employee_id = employees.id").
|
db = db.Joins("JOIN employee_kandangs ek ON ek.employee_id = employees.id").
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ type Update struct {
|
|||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=500,gt=0"`
|
||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
KandangId *uint `query:"kandang_id" validate:"omitempty"`
|
KandangId *uint `query:"kandang_id" validate:"omitempty"`
|
||||||
IsActive *bool `query:"is_active" validate:"omitempty"`
|
IsActive *bool `query:"is_active" validate:"omitempty"`
|
||||||
|
|||||||
@@ -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 {
|
fcrs, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
if params.Search != "" {
|
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")
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
})
|
})
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user