Feat[BE-261]: creating multiple Approval API, Update API, Delete API and some logic Adjustment

This commit is contained in:
aguhh18
2025-11-21 00:55:02 +07:00
parent b502751b4e
commit b8d1268dfa
8 changed files with 797 additions and 550 deletions
@@ -7,7 +7,9 @@ CREATE TABLE expenses (
), ),
po_number VARCHAR(50) NULL, po_number VARCHAR(50) NULL,
document_path JSON, document_path JSON,
realization_document_path JSON,
expense_date DATE NOT NULL, expense_date DATE NOT NULL,
realization_date DATE,
grand_total NUMERIC(15, 3) DEFAULT 0, grand_total NUMERIC(15, 3) DEFAULT 0,
note TEXT, note TEXT,
created_by BIGINT, created_by BIGINT,
+15 -13
View File
@@ -8,19 +8,21 @@ import (
) )
type Expense struct { type Expense struct {
Id uint64 `gorm:"primaryKey;autoIncrement"` Id uint64 `gorm:"primaryKey;autoIncrement"`
ReferenceNumber *string `gorm:"type:varchar(50);uniqueIndex"` ReferenceNumber string `gorm:"type:varchar(50);uniqueIndex"`
SupplierId *uint64 `gorm:""` SupplierId uint64 `gorm:""`
Category string `gorm:"type:varchar(50);not null"` Category string `gorm:"type:varchar(50);not null"`
PoNumber *string `gorm:"type:varchar(50)"` PoNumber string `gorm:"type:varchar(50)"`
DocumentPath sql.NullString `gorm:"type:json"` DocumentPath sql.NullString `gorm:"type:json"` // Dokumen pengajuan
ExpenseDate time.Time `gorm:"type:date;not null"` RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"` // Dokumen realisasi
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"` RealizationDate time.Time `gorm:"type:date;column:realization_date"` // Tanggal realisasi
Note *string `gorm:"type:text"` ExpenseDate time.Time `gorm:"type:date;not null"`
CreatedBy *uint64 `gorm:""` GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
CreatedAt time.Time `gorm:"autoCreateTime"` Note string `gorm:"type:text"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` CreatedBy uint64 `gorm:""`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// Relations // Relations
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"` Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
+4 -2
View File
@@ -10,11 +10,12 @@ 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:""`
NonstockId *uint64 `gorm:""` NonstockId *uint64 `gorm:""`
Qty float64 `gorm:"type:numeric(15,3);not null"` Qty float64 `gorm:"type:numeric(15,3);not null"`
UnitPrice float64 `gorm:"type:numeric(15,3);not null"` UnitPrice float64 `gorm:"type:numeric(15,3);not null"`
TotalPrice float64 `gorm:"type:numeric(15,3);not null"` TotalPrice float64 `gorm:"type:numeric(15,3);not null"`
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"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
@@ -22,6 +23,7 @@ type ExpenseNonstock struct {
// Relations // Relations
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"`
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"` Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
Realization *ExpenseRealization `gorm:"foreignKey:ExpenseNonstockId;references:Id"` Realization *ExpenseRealization `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
} }
@@ -103,11 +103,23 @@ func (u *ExpenseController) CreateOne(c *fiber.Ctx) error {
var singleCostPerKandang validation.CostPerKandang var singleCostPerKandang validation.CostPerKandang
if err := json.Unmarshal([]byte(costPerKandangJSON), &singleCostPerKandang); err != nil { if err := json.Unmarshal([]byte(costPerKandangJSON), &singleCostPerKandang); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid cost_per_kandang JSON: %v", err)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid cost_per_kandangs JSON: %v", err))
}
if singleCostPerKandang.KandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Field KandangID is required")
} }
req.CostPerKandangs = []validation.CostPerKandang{singleCostPerKandang} req.CostPerKandangs = []validation.CostPerKandang{singleCostPerKandang}
} else {
for i, costPerKandang := range req.CostPerKandangs {
if costPerKandang.KandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for cost_per_kandangs[%d]", i))
}
}
} }
} else {
return fiber.NewError(fiber.StatusBadRequest, "Field cost_per_kandangs is required")
} }
result, err := u.ExpenseService.CreateOne(c, req) result, err := u.ExpenseService.CreateOne(c, req)
@@ -133,8 +145,30 @@ func (u *ExpenseController) UpdateOne(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
} }
if err := c.BodyParser(req); err != nil { form, err := c.MultipartForm()
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
}
req.Documents = form.File["documents"]
if transactionDate := c.FormValue("transaction_date"); transactionDate != "" {
req.TransactionDate = &transactionDate
}
costPerKandangJSON := c.FormValue("cost_per_kandang")
if costPerKandangJSON != "" {
var costPerKandang []validation.CostPerKandang
if err := json.Unmarshal([]byte(costPerKandangJSON), &costPerKandang); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid cost_per_kandang JSON: %v", err))
}
for i, costPerKandang := range costPerKandang {
if costPerKandang.KandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for cost_per_kandang[%d]", i))
}
}
req.CostPerKandang = &costPerKandang
} }
result, err := u.ExpenseService.UpdateOne(c, req, uint(id)) result, err := u.ExpenseService.UpdateOne(c, req, uint(id))
@@ -171,42 +205,45 @@ func (u *ExpenseController) DeleteOne(c *fiber.Ctx) error {
}) })
} }
func (u *ExpenseController) ApproveExpense(c *fiber.Ctx) error { func (u *ExpenseController) Approval(c *fiber.Ctx) error {
expenseID := c.Params("id") req := new(validation.ApprovalRequest)
id, err := strconv.Atoi(expenseID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID")
}
// Extract step from URL path (manager or finance) if err := c.BodyParser(req); err != nil {
path := c.Path()
var step string
if strings.Contains(path, "/approvals/manager") {
step = "Manager"
} else if strings.Contains(path, "/approvals/finance") {
step = "Finance"
} else {
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval step")
}
// Parse approval request
var req validation.ApprovalRequest
if err := c.BodyParser(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
} }
// Approve expense path := c.Path()
expense, err := u.ExpenseService.ApproveExpense(c, uint(id), step, req.Action, req.Notes) approvalType := ""
if strings.Contains(path, "/approvals/manager") {
approvalType = "manager"
} else if strings.Contains(path, "/approvals/finance") {
approvalType = "finance"
} else {
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval path")
}
results, err := u.ExpenseService.Approval(c, req, approvalType)
if err != nil { if err != nil {
return err return err
} }
var (
data interface{}
message = "Submit expense approval successfully"
)
if len(results) == 1 {
data = results[0]
} else {
message = "Submit expense approvals successfully"
data = results
}
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: "Approve expense successfully", Message: message,
Data: expense, Data: data,
}) })
} }
@@ -224,8 +261,8 @@ func (u *ExpenseController) CreateRealization(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form") return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
} }
req.Documents = form.File["documents"] req.Documents = form.File["documents"]
req.RealizationDate = c.FormValue("realization_date")
// Parse realizations JSON
realizationsJSON := c.FormValue("realizations") realizationsJSON := c.FormValue("realizations")
if realizationsJSON != "" { if realizationsJSON != "" {
if err := json.Unmarshal([]byte(realizationsJSON), &req.Realizations); err != nil { if err := json.Unmarshal([]byte(realizationsJSON), &req.Realizations); err != nil {
@@ -255,8 +292,21 @@ func (u *ExpenseController) UpdateRealization(c *fiber.Ctx) error {
} }
var req validation.UpdateRealization var req validation.UpdateRealization
if err := c.BodyParser(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") form, err := c.MultipartForm()
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
}
req.Documents = form.File["documents"]
req.RealizationDate = c.FormValue("realization_date")
realizationsJSON := c.FormValue("realizations")
if realizationsJSON != "" {
if err := json.Unmarshal([]byte(realizationsJSON), &req.Realizations); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid realizations JSON: %v", err))
}
} }
expense, err := u.ExpenseService.UpdateRealization(c, uint(id), &req) expense, err := u.ExpenseService.UpdateRealization(c, uint(id), &req)
@@ -293,3 +343,49 @@ func (u *ExpenseController) CompleteExpense(c *fiber.Ctx) error {
Data: expense, Data: expense,
}) })
} }
func (u *ExpenseController) DeleteDocument(c *fiber.Ctx) error {
expenseID, err := strconv.Atoi(c.Params("id"))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID")
}
documentID, err := strconv.ParseUint(c.Params("documentId"), 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid document ID")
}
if err := u.ExpenseService.DeleteDocument(c, uint(expenseID), documentID, false); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete document successfully",
})
}
func (u *ExpenseController) DeleteRealizationDocument(c *fiber.Ctx) error {
expenseID, err := strconv.Atoi(c.Params("id"))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID")
}
documentID, err := strconv.ParseUint(c.Params("documentId"), 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid document ID")
}
if err := u.ExpenseService.DeleteDocument(c, uint(expenseID), documentID, true); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete realization document successfully",
})
}
+120 -170
View File
@@ -2,7 +2,6 @@ package dto
import ( import (
"encoding/json" "encoding/json"
"fmt"
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -13,21 +12,18 @@ import (
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
) )
// === Base DTO ===
type ExpenseBaseDTO struct { type ExpenseBaseDTO struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
ReferenceNumber string `json:"reference_number"` ReferenceNumber string `json:"reference_number"`
PoNumber *string `json:"po_number"` PoNumber string `json:"po_number"`
Category string `json:"category"` Category string `json:"category"`
Documents []string `json:"documents,omitempty"` Supplier *supplierDTO.SupplierBaseDTO `json:"supplier,omitempty"`
ExpenseDate time.Time `json:"expense_date"` RealizationDate *time.Time `json:"realization_date,omitempty"`
GrandTotal float64 `json:"grand_total"` ExpenseDate time.Time `json:"expense_date"`
GrandTotal float64 `json:"grand_total"`
Location *locationDTO.LocationBaseDTO `json:"location,omitempty"` Location *locationDTO.LocationBaseDTO `json:"location,omitempty"`
} }
// === List DTO (untuk GetAll) ===
type ExpenseListDTO struct { type ExpenseListDTO struct {
ExpenseBaseDTO ExpenseBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
@@ -36,95 +32,59 @@ type ExpenseListDTO struct {
LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"` LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"`
} }
// === Detail DTO (untuk GetOne) ===
type ExpenseDetailDTO struct { type ExpenseDetailDTO struct {
ExpenseBaseDTO ExpenseBaseDTO
Supplier *supplierDTO.SupplierBaseDTO `json:"supplier,omitempty"` Documents []DocumentDTO `json:"documents,omitempty"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` RealizationDocs []DocumentDTO `json:"realization_docs,omitempty"`
Kandangs []KandangGroupDTO `json:"kandangs,omitempty"` CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
TotalPengajuan float64 `json:"total_pengajuan"` Kandangs []KandangGroupDTO `json:"kandangs,omitempty"`
TotalRealisasi float64 `json:"total_realisasi"` TotalPengajuan float64 `json:"total_pengajuan"`
CreatedAt time.Time `json:"created_at"` TotalRealisasi float64 `json:"total_realisasi"`
UpdatedAt time.Time `json:"updated_at"` CreatedAt time.Time `json:"created_at"`
LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"` UpdatedAt time.Time `json:"updated_at"`
LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"`
} }
// === Nested DTO ===
type ExpenseNonstockDTO struct { type ExpenseNonstockDTO struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
Qty float64 `json:"qty"` Qty float64 `json:"qty"`
UnitPrice float64 `json:"unit_price"` UnitPrice float64 `json:"unit_price"`
TotalPrice float64 `json:"total_price"` TotalPrice float64 `json:"total_price"`
Note *string `json:"note,omitempty"` Note *string `json:"note,omitempty"`
Nonstock *nonstockDTO.NonstockBaseDTO `json:"nonstock,omitempty"` Nonstock *nonstockDTO.NonstockBaseDTO `json:"nonstock,omitempty"`
ProjectFlockKandang *ProjectFlockKandangNestedDTO `json:"project_flock_kandang,omitempty"`
Realization *ExpenseRealizationDTO `json:"realization,omitempty"`
}
type ProjectFlockKandangNestedDTO struct {
Id uint64 `json:"id"`
KandangId uint64 `json:"kandang_id"`
} }
type ExpenseRealizationDTO struct { type ExpenseRealizationDTO struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
Qty float64 `json:"qty"` Qty float64 `json:"qty"`
UnitPrice float64 `json:"unit_price"` UnitPrice float64 `json:"unit_price"`
TotalPrice float64 `json:"total_price"` TotalPrice float64 `json:"total_price"`
Date time.Time `json:"date"` Note *string `json:"note,omitempty"`
Note *string `json:"note,omitempty"` Nonstock *nonstockDTO.NonstockBaseDTO `json:"nonstock,omitempty"`
}
type RealizationOnlyDTO struct {
Id uint64 `json:"id"`
Qty float64 `json:"qty"`
UnitPrice float64 `json:"unit_price"`
TotalPrice float64 `json:"total_price"`
Date time.Time `json:"date"`
Note *string `json:"note,omitempty"`
Nonstock *nonstockDTO.NonstockBaseDTO `json:"nonstock,omitempty"`
ProjectFlockKandang *ProjectFlockKandangNestedDTO `json:"project_flock_kandang,omitempty"`
}
type KandangDTO struct {
Id uint64 `json:"id"`
KandangId uint64 `json:"kandang_id"`
Name string `json:"name,omitempty"`
} }
type KandangGroupDTO struct { type KandangGroupDTO struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
KandangId uint64 `json:"kandang_id"` KandangId uint64 `json:"kandang_id"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Pengajuans []ExpenseNonstockDTO `json:"pengajuans,omitempty"` Pengajuans []ExpenseNonstockDTO `json:"pengajuans,omitempty"`
Realisasi []RealizationOnlyDTO `json:"realisasi,omitempty"` Realisasi []ExpenseRealizationDTO `json:"realisasi,omitempty"`
} }
// === Helper Functions === type DocumentDTO struct {
ID uint64 `json:"id"`
func getStringValue(s *string) string { Path string `json:"path"`
if s == nil {
return ""
}
return *s
} }
// === Mapper Functions ===
func ToExpenseBaseDTO(e *entity.Expense) ExpenseBaseDTO { func ToExpenseBaseDTO(e *entity.Expense) ExpenseBaseDTO {
var documents []string
var location *locationDTO.LocationBaseDTO var location *locationDTO.LocationBaseDTO
var supplier *supplierDTO.SupplierBaseDTO
// Parse document paths from JSON if available var realizationDate *time.Time
if e.DocumentPath.Valid && e.DocumentPath.String != "" { if !e.RealizationDate.IsZero() {
if err := json.Unmarshal([]byte(e.DocumentPath.String), &documents); err == nil { realizationDate = &e.RealizationDate
// Successfully parsed documents
}
} }
// Get location from the first kandang if available
if len(e.Nonstocks) > 0 && e.Nonstocks[0].ProjectFlockKandang != nil { if len(e.Nonstocks) > 0 && e.Nonstocks[0].ProjectFlockKandang != nil {
if e.Nonstocks[0].ProjectFlockKandang.Kandang.Location.Id != 0 { if e.Nonstocks[0].ProjectFlockKandang.Kandang.Location.Id != 0 {
mapped := locationDTO.ToLocationBaseDTO(e.Nonstocks[0].ProjectFlockKandang.Kandang.Location) mapped := locationDTO.ToLocationBaseDTO(e.Nonstocks[0].ProjectFlockKandang.Kandang.Location)
@@ -132,12 +92,18 @@ func ToExpenseBaseDTO(e *entity.Expense) ExpenseBaseDTO {
} }
} }
if e.Supplier != nil && e.Supplier.Id != 0 {
mapped := supplierDTO.ToSupplierBaseDTO(*e.Supplier)
supplier = &mapped
}
return ExpenseBaseDTO{ return ExpenseBaseDTO{
Id: e.Id, Id: e.Id,
ReferenceNumber: getStringValue(e.ReferenceNumber), ReferenceNumber: e.ReferenceNumber,
PoNumber: e.PoNumber, // Keep as pointer to allow null in JSON PoNumber: e.PoNumber,
Category: e.Category, Category: e.Category,
Documents: documents, Supplier: supplier,
RealizationDate: realizationDate,
ExpenseDate: e.ExpenseDate, ExpenseDate: e.ExpenseDate,
GrandTotal: e.GrandTotal, GrandTotal: e.GrandTotal,
Location: location, Location: location,
@@ -175,18 +141,14 @@ func ToExpenseListDTOs(expenses []entity.Expense) []ExpenseListDTO {
} }
func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO { func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
var documents []DocumentDTO
var realizationDocs []DocumentDTO
var createdUser *userDTO.UserBaseDTO var createdUser *userDTO.UserBaseDTO
if e.CreatedUser != nil && e.CreatedUser.Id != 0 { if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) mapped := userDTO.ToUserBaseDTO(*e.CreatedUser)
createdUser = &mapped createdUser = &mapped
} }
var supplier *supplierDTO.SupplierBaseDTO
if e.Supplier != nil && e.Supplier.Id != 0 {
mapped := supplierDTO.ToSupplierBaseDTO(*e.Supplier)
supplier = &mapped
}
var latestApproval *approvalDTO.ApprovalBaseDTO var latestApproval *approvalDTO.ApprovalBaseDTO
if e.LatestApproval != nil { if e.LatestApproval != nil {
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
@@ -194,51 +156,45 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
} }
var pengajuans []ExpenseNonstockDTO var pengajuans []ExpenseNonstockDTO
var realisasi []RealizationOnlyDTO var realisasi []ExpenseRealizationDTO
if e.DocumentPath.Valid && e.DocumentPath.String != "" {
json.Unmarshal([]byte(e.DocumentPath.String), &documents)
}
if e.RealizationDocumentPath.Valid && e.RealizationDocumentPath.String != "" {
json.Unmarshal([]byte(e.RealizationDocumentPath.String), &realizationDocs)
}
if len(e.Nonstocks) > 0 { if len(e.Nonstocks) > 0 {
pengajuans = make([]ExpenseNonstockDTO, 0) pengajuans = make([]ExpenseNonstockDTO, 0)
realisasi = make([]RealizationOnlyDTO, 0) realisasi = make([]ExpenseRealizationDTO, 0)
for _, ns := range e.Nonstocks { for _, ns := range e.Nonstocks {
// Create DTO without realization for pengajuans
pengajuanDTO := ToExpenseNonstockDTO(ns) pengajuanDTO := ToExpenseNonstockDTO(ns)
pengajuanDTO.Realization = nil // Remove realization from pengajuan
pengajuans = append(pengajuans, pengajuanDTO) pengajuans = append(pengajuans, pengajuanDTO)
// Create separate DTO with realization data if it exists
if ns.Realization != nil && ns.Realization.Id != 0 { if ns.Realization != nil && ns.Realization.Id != 0 {
// Create realization DTO with only realization data
var nonstock *nonstockDTO.NonstockBaseDTO var nonstock *nonstockDTO.NonstockBaseDTO
if ns.Nonstock != nil && ns.Nonstock.Id != 0 { if ns.Nonstock != nil && ns.Nonstock.Id != 0 {
mapped := nonstockDTO.ToNonstockBaseDTO(*ns.Nonstock) mapped := nonstockDTO.ToNonstockBaseDTO(*ns.Nonstock)
nonstock = &mapped nonstock = &mapped
} }
var projectFlockKandang *ProjectFlockKandangNestedDTO realisasiDTO := ExpenseRealizationDTO{
if ns.ProjectFlockKandang != nil && ns.ProjectFlockKandang.Id != 0 { Id: ns.Realization.Id,
projectFlockKandang = &ProjectFlockKandangNestedDTO{ Qty: ns.Realization.RealizationQty,
Id: uint64(ns.ProjectFlockKandang.Id), UnitPrice: ns.Realization.RealizationUnitPrice,
KandangId: uint64(ns.ProjectFlockKandang.KandangId), TotalPrice: ns.Realization.RealizationTotalPrice,
} Note: ns.Realization.Note,
} Nonstock: nonstock,
realisasiDTO := RealizationOnlyDTO{
Id: ns.Realization.Id,
Qty: ns.Realization.RealizationQty,
UnitPrice: ns.Realization.RealizationUnitPrice,
TotalPrice: ns.Realization.RealizationTotalPrice,
Date: ns.Realization.RealizationDate,
Note: ns.Realization.Note,
Nonstock: nonstock,
ProjectFlockKandang: projectFlockKandang,
} }
realisasi = append(realisasi, realisasiDTO) realisasi = append(realisasi, realisasiDTO)
} }
} }
} }
// Calculate total pengajuan and realisasi
var totalPengajuan float64 var totalPengajuan float64
for _, p := range pengajuans { for _, p := range pengajuans {
totalPengajuan += p.TotalPrice totalPengajuan += p.TotalPrice
@@ -249,55 +205,76 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
totalRealisasi += r.TotalPrice totalRealisasi += r.TotalPrice
} }
// Group pengajuans and realisasi by kandang
kandangMap := make(map[uint64]*KandangGroupDTO) kandangMap := make(map[uint64]*KandangGroupDTO)
// Process pengajuans
for _, p := range pengajuans { for _, p := range pengajuans {
if p.ProjectFlockKandang != nil { var kandangId uint64
kandangId := p.ProjectFlockKandang.KandangId var kandangName string
for _, ns := range e.Nonstocks {
if ns.Id == p.Id && ns.Kandang != nil {
kandangId = uint64(ns.Kandang.Id)
kandangName = ns.Kandang.Name
break
}
}
if kandangId > 0 {
if kandangMap[kandangId] == nil { if kandangMap[kandangId] == nil {
kandangMap[kandangId] = &KandangGroupDTO{ kandangMap[kandangId] = &KandangGroupDTO{
Id: p.ProjectFlockKandang.Id, Id: kandangId,
KandangId: kandangId, KandangId: kandangId,
Name: fmt.Sprintf("Kandang %d", kandangId), Name: kandangName,
Pengajuans: []ExpenseNonstockDTO{},
Realisasi: []ExpenseRealizationDTO{},
} }
} }
kandangMap[kandangId].Pengajuans = append(kandangMap[kandangId].Pengajuans, p) kandangMap[kandangId].Pengajuans = append(kandangMap[kandangId].Pengajuans, p)
} }
} }
// Process realisasi
for _, r := range realisasi { for _, r := range realisasi {
if r.ProjectFlockKandang != nil { var kandangId uint64
kandangId := r.ProjectFlockKandang.KandangId var kandangName string
for _, ns := range e.Nonstocks {
if ns.Realization != nil && ns.Realization.Id == r.Id && ns.Kandang != nil {
kandangId = uint64(ns.Kandang.Id)
kandangName = ns.Kandang.Name
break
}
}
if kandangId > 0 {
if kandangMap[kandangId] == nil { if kandangMap[kandangId] == nil {
kandangMap[kandangId] = &KandangGroupDTO{ kandangMap[kandangId] = &KandangGroupDTO{
Id: r.ProjectFlockKandang.Id, Id: kandangId,
KandangId: kandangId, KandangId: kandangId,
Name: fmt.Sprintf("Kandang %d", kandangId), Name: kandangName,
Pengajuans: []ExpenseNonstockDTO{},
Realisasi: []ExpenseRealizationDTO{},
} }
} }
kandangMap[kandangId].Realisasi = append(kandangMap[kandangId].Realisasi, r) kandangMap[kandangId].Realisasi = append(kandangMap[kandangId].Realisasi, r)
} }
} }
// Convert map to slice
var kandangs []KandangGroupDTO var kandangs []KandangGroupDTO
for _, k := range kandangMap { for _, k := range kandangMap {
kandangs = append(kandangs, *k) kandangs = append(kandangs, *k)
} }
return ExpenseDetailDTO{ return ExpenseDetailDTO{
ExpenseBaseDTO: ToExpenseBaseDTO(e), ExpenseBaseDTO: ToExpenseBaseDTO(e),
Supplier: supplier, Documents: documents,
CreatedUser: createdUser, RealizationDocs: realizationDocs,
Kandangs: kandangs, CreatedUser: createdUser,
TotalPengajuan: totalPengajuan, Kandangs: kandangs,
TotalRealisasi: totalRealisasi, TotalPengajuan: totalPengajuan,
CreatedAt: e.CreatedAt, TotalRealisasi: totalRealisasi,
UpdatedAt: e.UpdatedAt, CreatedAt: e.CreatedAt,
LatestApproval: latestApproval, UpdatedAt: e.UpdatedAt,
LatestApproval: latestApproval,
} }
} }
@@ -308,39 +285,12 @@ func ToExpenseNonstockDTO(ns entity.ExpenseNonstock) ExpenseNonstockDTO {
nonstock = &mapped nonstock = &mapped
} }
var projectFlockKandang *ProjectFlockKandangNestedDTO
if ns.ProjectFlockKandang != nil && ns.ProjectFlockKandang.Id != 0 {
projectFlockKandang = &ProjectFlockKandangNestedDTO{
Id: uint64(ns.ProjectFlockKandang.Id),
KandangId: uint64(ns.ProjectFlockKandang.KandangId),
}
}
var realization *ExpenseRealizationDTO
if ns.Realization != nil && ns.Realization.Id != 0 {
mapped := ToExpenseRealizationDTO(*ns.Realization)
realization = &mapped
}
return ExpenseNonstockDTO{ return ExpenseNonstockDTO{
Id: ns.Id, Id: ns.Id,
Qty: ns.Qty, Qty: ns.Qty,
UnitPrice: ns.UnitPrice, UnitPrice: ns.UnitPrice,
TotalPrice: ns.TotalPrice, TotalPrice: ns.TotalPrice,
Note: ns.Note, Note: &ns.Note,
Nonstock: nonstock, Nonstock: nonstock,
ProjectFlockKandang: projectFlockKandang,
Realization: realization,
}
}
func ToExpenseRealizationDTO(r entity.ExpenseRealization) ExpenseRealizationDTO {
return ExpenseRealizationDTO{
Id: r.Id,
Qty: r.RealizationQty,
UnitPrice: r.RealizationUnitPrice,
TotalPrice: r.RealizationTotalPrice,
Date: r.RealizationDate,
Note: r.Note,
} }
} }
+4 -2
View File
@@ -25,9 +25,11 @@ func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService
route.Get("/:id", ctrl.GetOne) route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne) route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne) route.Delete("/:id", ctrl.DeleteOne)
route.Post("/:id/approvals/manager", ctrl.ApproveExpense) route.Post("/approvals/manager", ctrl.Approval)
route.Post("/:id/approvals/finance", ctrl.ApproveExpense) route.Post("/approvals/finance", ctrl.Approval)
route.Post("/:id/realizations", ctrl.CreateRealization) route.Post("/:id/realizations", ctrl.CreateRealization)
route.Patch("/:id/realizations", ctrl.UpdateRealization) route.Patch("/:id/realizations", ctrl.UpdateRealization)
route.Post("/:id/complete", ctrl.CompleteExpense) route.Post("/:id/complete", ctrl.CompleteExpense)
route.Delete("/:id/documents/:documentId", ctrl.DeleteDocument)
route.Delete("/:id/realization-documents/:documentId", ctrl.DeleteRealizationDocument)
} }
File diff suppressed because it is too large Load Diff
@@ -4,39 +4,31 @@ import (
"mime/multipart" "mime/multipart"
) )
// ApprovalRequest is used for expense approval endpoints
type ApprovalRequest struct {
Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"`
Notes *string `json:"notes"`
}
type Create struct { type Create struct {
PoNumber *string `form:"po_number" validate:"omitempty,max=50"` PoNumber string `form:"po_number" json:"po_number" validate:"omitempty,max=50"`
TransactionDate string `form:"transaction_date" validate:"required,datetime=2006-01-02"` TransactionDate string `form:"transaction_date" json:"transaction_date" validate:"required,datetime=2006-01-02"`
Category string `form:"category" validate:"required,oneof=BOP NON-BOP"` Category string `form:"category" json:"category" validate:"required,oneof=BOP NON-BOP"`
SupplierID uint64 `form:"supplier_id" validate:"required,gt=0"` SupplierID uint64 `form:"supplier_id" json:"supplier_id" validate:"required,gt=0"`
Documents []*multipart.FileHeader `form:"documents" validate:"omitempty,dive"` Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
CostPerKandangs []CostPerKandang `form:"cost_per_kandangs" validate:"required,min=1,dive"` CostPerKandangs []CostPerKandang `form:"cost_per_kandangs" json:"cost_per_kandangs" validate:"required,min=1,dive"`
} }
type CostPerKandang struct { type CostPerKandang struct {
KandangID uint64 `json:"kandang_id" form:"kandang_id" validate:"required,gt=0"` KandangID uint64 `form:"kandang_id" json:"kandang_id" validate:"required,gt=0"`
CostItems []CostItem `json:"cost_items" form:"cost_items" validate:"required,min=1,dive"` CostItems []CostItem `form:"cost_items" json:"cost_items" validate:"required,min=1,dive"`
} }
type CostItem struct { type CostItem struct {
NonstockID uint64 `json:"nonstock_id" form:"nonstock_id" validate:"required,gt=0"` NonstockID uint64 `form:"nonstock_id" json:"nonstock_id" validate:"required,gt=0"`
Quantity float64 `json:"quantity" form:"quantity" validate:"required,gt=0"` Quantity float64 `form:"quantity" json:"quantity" validate:"required,gt=0"`
TotalCost float64 `json:"total_cost" form:"total_cost" validate:"required,gt=0"` TotalCost float64 `form:"total_cost" json:"total_cost" validate:"required,gt=0"`
Notes string `json:"notes" form:"notes" validate:"required,max=500"` Notes string `form:"notes" json:"notes" validate:"required,max=500"`
} }
type Update struct { type Update struct {
PoNumber *string `json:"po_number,omitempty" validate:"omitempty,max=50"` TransactionDate *string `form:"transaction_date" json:"transaction_date" validate:"omitempty,datetime=2006-01-02"`
TransactionDate *string `json:"transaction_date,omitempty" validate:"omitempty,datetime=2006-01-02"` CostPerKandang *[]CostPerKandang `form:"cost_per_kandang" json:"cost_per_kandang" validate:"omitempty,min=1,dive"`
SupplierID *uint64 `json:"supplier_id,omitempty" validate:"omitempty,gt=0"` Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
Documents *[]string `json:"documents,omitempty" validate:"omitempty,dive,max=255"`
CostPerKandang *[]CostPerKandang `json:"cost_per_kandang,omitempty" validate:"omitempty,min=1,dive"`
} }
type Query struct { type Query struct {
@@ -46,21 +38,27 @@ type Query struct {
} }
type CreateRealization struct { type CreateRealization struct {
Documents []*multipart.FileHeader `form:"documents" validate:"omitempty,dive"` RealizationDate string `form:"realization_date" json:"realization_date" validate:"required,datetime=2006-01-02"`
Realizations []RealizationItem `json:"realizations" form:"realizations" validate:"required,min=1,dive"` Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
} Realizations []RealizationItem `form:"realizations" json:"realizations" validate:"required,min=1,dive"`
type RealizationItem struct {
ExpenseNonstockID uint64 `json:"expense_nonstock_id" form:"expense_nonstock_id" validate:"required,gt=0"`
Qty float64 `json:"qty" form:"qty" validate:"required,gt=0"`
UnitPrice float64 `json:"unit_price" form:"unit_price" validate:"required,gt=0"`
TotalPrice float64 `json:"total_price" form:"total_price" validate:"required,gt=0"`
Notes *string `json:"notes" form:"notes" validate:"omitempty,max=500"`
}
type CompleteExpense struct {
} }
type UpdateRealization struct { type UpdateRealization struct {
Realizations []RealizationItem `json:"realizations" validate:"required,min=1,dive"` RealizationDate string `form:"realization_date" json:"realization_date" validate:"omitempty,datetime=2006-01-02"`
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
Realizations []RealizationItem `form:"realizations" json:"realizations" validate:"required,min=1,dive"`
}
type RealizationItem struct {
ExpenseNonstockID uint64 `form:"expense_nonstock_id" json:"expense_nonstock_id" validate:"required,gt=0"`
Qty float64 `form:"qty" json:"qty" validate:"required,gt=0"`
UnitPrice float64 `form:"unit_price" json:"unit_price" validate:"required,gt=0"`
TotalPrice float64 `form:"total_price" json:"total_price" validate:"required,gt=0"`
Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"`
}
type ApprovalRequest struct {
Action string `json:"action" form:"action" validate:"required,oneof=APPROVED REJECTED"`
ApprovableIds []uint `json:"approvable_ids" validate:"required,min=1,dive,gt=0"`
Notes *string `json:"notes" form:"notes"`
} }