package repository import ( "context" "database/sql" "math" "strconv" "strings" "time" ) type PaketListParams struct { Search string Code string CareType string Status int Active string StartDate string EndDate string Page int PerPage int } type PaketPage struct { Items []PaketSummary Total int } type PaketSummary struct { PaketID string `json:"PaketID"` KodeCorp *string `json:"KodeCorp"` Nama string `json:"Nama"` JenisPaketID string `json:"JenisPaketID"` JenisPaket *string `json:"JenisPaket"` Harga int64 `json:"Harga"` RawatInap bool `json:"RawatInap"` RawatJalan bool `json:"RawatJalan"` MultiVisit bool `json:"MultiVisit"` TglDari string `json:"TglDari"` TglSampai string `json:"TglSampai"` KelasID string `json:"KelasID"` Kelas *string `json:"Kelas"` JumlahHari int `json:"JumlahHari"` StatusID int `json:"StatusID"` Status *string `json:"Status"` NA string `json:"NA"` Aktif bool `json:"Aktif"` Keterangan string `json:"Keterangan"` } type PaketDetail struct { PaketID string `json:"PaketID"` Nama string `json:"Nama"` JenisPaket string `json:"JenisPaket"` RJ int `json:"RJ"` RI int `json:"RI"` TglDari string `json:"TglDari"` TglSampai string `json:"TglSampai"` MultiVisit int `json:"MultiVisit"` HeaderPaket string `json:"HeaderPaket"` KelasID string `json:"KelasID"` JumlahHari int `json:"JumlahHari"` TotalHarga string `json:"TotalHarga"` Keterangan string `json:"Keterangan"` StatusID int `json:"StatusID"` LoginBuat string `json:"LoginBuat"` TanggalBuat string `json:"TanggalBuat"` LoginEdit string `json:"LoginEdit"` TanggalEdit string `json:"TanggalEdit"` NA string `json:"NA"` Layanan []PaketLayanan `json:"layanan"` Operasi []PaketOperasi `json:"operasi"` Item []PaketItem `json:"item"` Kamar []PaketKamar `json:"kamar"` } type PaketLayanan struct { PaketLayananID int64 `json:"PaketLayananID"` LayananID string `json:"LayananID"` NamaLayanan *string `json:"NamaLayanan"` NamaDepartemen *string `json:"NamaDepartemen"` Jumlah string `json:"Jumlah"` JumlahHari int `json:"JumlahHari"` Harga string `json:"Harga"` Diskon string `json:"Diskon"` TipeDiskon string `json:"TipeDiskon"` ShareRS string `json:"ShareRS"` ShareDokter string `json:"ShareDokter"` Subtotal string `json:"Subtotal"` } type PaketOperasi struct { PaketOperasiHeaderID int64 `json:"PaketOperasiHeaderID"` NamaTindakan *string `json:"NamaTindakan"` NamaGolongan *string `json:"NamaGolongan"` NamaJenisAnestesi *string `json:"NamaJenisAnestesi"` NamaKasusOperasi *string `json:"NamaKasusOperasi"` EstimasiWaktu string `json:"EstimasiWaktu"` Keterangan string `json:"Keterangan"` } type PaketItem struct { PaketItemID int64 `json:"PaketItemID"` ItemID string `json:"ItemID"` NamaItem *string `json:"NamaItem"` NamaDepartemen *string `json:"NamaDepartemen"` Jumlah string `json:"Jumlah"` NamaSatuan *string `json:"NamaSatuan"` Harga string `json:"Harga"` Diskon string `json:"Diskon"` TipeDiskon string `json:"TipeDiskon"` SubTotal string `json:"SubTotal"` } type PaketKamar struct { PaketTTID int64 `json:"PaketTTID"` NamaDepartemen *string `json:"NamaDepartemen"` NamaKelas *string `json:"NamaKelas"` Jumlah string `json:"Jumlah"` Harga string `json:"Harga"` TipeDiskon string `json:"TipeDiskon"` Diskon float64 `json:"Diskon"` SubTotal string `json:"SubTotal"` } func (r MySQLLayananRepository) ListPaket(ctx context.Context, params PaketListParams) (PaketPage, error) { where, args := paketListWhere(params) var total int if err := r.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM paket_header ph "+where, args...).Scan(&total); err != nil { return PaketPage{}, err } query := ` SELECT ph.PaketID, (SELECT pch.KodeCorp FROM paket_corp_header pch WHERE pch.PaketID = ph.PaketID ORDER BY pch.TanggalBuat DESC LIMIT 1), ph.Nama, ph.JenisPaket, jp.Nama, ph.TotalHarga, ph.RI, ph.RJ, ph.MultiVisit, CAST(ph.TglDari AS CHAR), CAST(ph.TglSampai AS CHAR), ph.KelasID, ph.JumlahHari, ph.StatusID, ph.NA, ph.Keterangan FROM paket_header ph LEFT JOIN jenispaket jp ON jp.JenisPaketID = ph.JenisPaket ` + where + ` ORDER BY ph.TanggalBuat DESC, ph.PaketID DESC LIMIT ? OFFSET ?` queryArgs := append(append([]any{}, args...), params.PerPage, (params.Page-1)*params.PerPage) rows, err := r.db.QueryContext(ctx, query, queryArgs...) if err != nil { return PaketPage{}, err } defer rows.Close() items := make([]PaketSummary, 0) for rows.Next() { var item PaketSummary var kodeCorp, jenisPaket sql.NullString var harga float64 var ri, rj, multiVisit int if err := rows.Scan( &item.PaketID, &kodeCorp, &item.Nama, &item.JenisPaketID, &jenisPaket, &harga, &ri, &rj, &multiVisit, &item.TglDari, &item.TglSampai, &item.KelasID, &item.JumlahHari, &item.StatusID, &item.NA, &item.Keterangan, ); err != nil { return PaketPage{}, err } item.KodeCorp = nullStringPtr(kodeCorp) item.JenisPaket = nullStringPtr(jenisPaket) item.Harga = int64(math.Round(harga)) item.RawatInap = ri != 0 item.RawatJalan = rj != 0 item.MultiVisit = multiVisit != 0 item.Status = paketStatus(item.StatusID) item.Aktif = item.NA == "N" items = append(items, item) } if err := rows.Err(); err != nil { return PaketPage{}, err } return PaketPage{Items: items, Total: total}, nil } func paketListWhere(params PaketListParams) (string, []any) { conditions := []string{"ph.StatusID = ?"} args := []any{params.Status} if params.Active != "" { conditions = append(conditions, "ph.NA = ?") args = append(args, params.Active) } if params.Search != "" { conditions = append(conditions, "ph.Nama LIKE ?") args = append(args, "%"+params.Search+"%") } if params.Code != "" { conditions = append(conditions, "ph.PaketID LIKE ?") args = append(args, "%"+params.Code+"%") } if params.CareType != "" { conditions = append(conditions, "ph."+params.CareType+" = 1") } if params.StartDate != "" { conditions = append(conditions, "ph.TglSampai >= ?") args = append(args, params.StartDate) } if params.EndDate != "" { conditions = append(conditions, "ph.TglDari <= ?") args = append(args, params.EndDate) } return "WHERE " + strings.Join(conditions, " AND "), args } func paketStatus(status int) *string { statuses := map[int]string{-1: "rejected", 0: "draft", 1: "submitted", 2: "approved"} value, ok := statuses[status] if !ok { return nil } return &value } func (r MySQLLayananRepository) GetPaket(ctx context.Context, id string) (*PaketDetail, error) { row := r.db.QueryRowContext(ctx, ` SELECT PaketID, Nama, JenisPaket, RJ, RI, CAST(TglDari AS CHAR), CAST(TglSampai AS CHAR), MultiVisit, HeaderPaket, KelasID, JumlahHari, CAST(TotalHarga AS CHAR), Keterangan, StatusID, LoginBuat, CAST(TanggalBuat AS CHAR), LoginEdit, CAST(TanggalEdit AS CHAR), NA FROM paket_header WHERE PaketID = ? LIMIT 1`, id) var result PaketDetail var tanggalBuat, tanggalEdit string if err := row.Scan( &result.PaketID, &result.Nama, &result.JenisPaket, &result.RJ, &result.RI, &result.TglDari, &result.TglSampai, &result.MultiVisit, &result.HeaderPaket, &result.KelasID, &result.JumlahHari, &result.TotalHarga, &result.Keterangan, &result.StatusID, &result.LoginBuat, &tanggalBuat, &result.LoginEdit, &tanggalEdit, &result.NA, ); err != nil { if err == sql.ErrNoRows { return nil, nil } return nil, err } result.TanggalBuat = laravelDateJSON(tanggalBuat) result.TanggalEdit = laravelDateJSON(tanggalEdit) var err error if result.Layanan, err = r.paketLayanan(ctx, id); err != nil { return nil, err } if result.Operasi, err = r.paketOperasi(ctx, id); err != nil { return nil, err } if result.Item, err = r.paketItem(ctx, id); err != nil { return nil, err } if result.Kamar, err = r.paketKamar(ctx, id); err != nil { return nil, err } return &result, nil } func (r MySQLLayananRepository) paketLayanan(ctx context.Context, id string) ([]PaketLayanan, error) { rows, err := r.db.QueryContext(ctx, ` SELECT pl.PaketLayananID, pl.LayananID, ml.Nama, d.Nama, CAST(pl.Jumlah AS CHAR), pl.JumlahHari, CAST(pl.Harga AS CHAR), CAST(pl.Diskon AS CHAR), pl.TipeDiskon, CAST(pl.ShareRS AS CHAR), CAST(pl.ShareDokter AS CHAR), CAST(pl.Subtotal AS CHAR) FROM paket_layanan pl LEFT JOIN masterlayanan ml ON ml.LayananID = pl.LayananID LEFT JOIN departemen d ON d.DepartemenID = pl.DepartemenID AND d.NA = 'N' WHERE pl.PaketID = ? AND pl.NA = 'N' ORDER BY pl.PaketLayananID`, id) if err != nil { return nil, err } defer rows.Close() result := make([]PaketLayanan, 0) for rows.Next() { var item PaketLayanan var namaLayanan, namaDepartemen sql.NullString var harga, shareRS, shareDokter, subtotal string if err := rows.Scan( &item.PaketLayananID, &item.LayananID, &namaLayanan, &namaDepartemen, &item.Jumlah, &item.JumlahHari, &harga, &item.Diskon, &item.TipeDiskon, &shareRS, &shareDokter, &subtotal, ); err != nil { return nil, err } item.NamaLayanan = nullStringPtr(namaLayanan) item.NamaDepartemen = nullStringPtr(namaDepartemen) item.Harga = phpNumberFormat(parseDecimal(harga), 0) item.ShareRS = phpNumberFormat(parseDecimal(shareRS), 2) item.ShareDokter = phpNumberFormat(parseDecimal(shareDokter), 0) item.Subtotal = phpNumberFormat(parseDecimal(subtotal), 0) result = append(result, item) } return result, rows.Err() } func (r MySQLLayananRepository) paketOperasi(ctx context.Context, id string) ([]PaketOperasi, error) { rows, err := r.db.QueryContext(ctx, ` SELECT po.PaketOperasiHeaderID, mt.Nama, go.Nama, ja.Nama, ko.Nama, po.EstimasiWaktu, po.Keterangan FROM paket_operasi_header po LEFT JOIN mastertindakan mt ON mt.TindakanID = po.TindakanID LEFT JOIN golonganoperasi go ON go.GolonganOperasiID = po.GolonganOperasiID LEFT JOIN jenisanestesi ja ON ja.JenisAnestesiID = po.JenisAnestesiID LEFT JOIN kasusoperasi ko ON ko.KasusOperasiID = po.KasusOperasiID WHERE po.PaketID = ? AND po.NA = 'N' ORDER BY po.PaketOperasiHeaderID`, id) if err != nil { return nil, err } defer rows.Close() result := make([]PaketOperasi, 0) for rows.Next() { var item PaketOperasi var tindakan, golongan, anestesi, kasus sql.NullString if err := rows.Scan( &item.PaketOperasiHeaderID, &tindakan, &golongan, &anestesi, &kasus, &item.EstimasiWaktu, &item.Keterangan, ); err != nil { return nil, err } item.NamaTindakan = nullStringPtr(tindakan) item.NamaGolongan = nullStringPtr(golongan) item.NamaJenisAnestesi = nullStringPtr(anestesi) item.NamaKasusOperasi = nullStringPtr(kasus) result = append(result, item) } return result, rows.Err() } func (r MySQLLayananRepository) paketItem(ctx context.Context, id string) ([]PaketItem, error) { rows, err := r.db.QueryContext(ctx, ` SELECT pi.PaketItemID, pi.ItemID, mi.Nama, d.Nama, CAST(pi.Jumlah AS CHAR), s.Nama, CAST(pi.Harga AS CHAR), CAST(pi.Diskon AS CHAR), pi.TipeDiskon, pi.SatuanID, pi.SatuanDasarID, COALESCE((SELECT ks.NilaiKonversi FROM konversisatuan ks WHERE ks.ItemID = pi.ItemID AND ks.SatuanTujuan = pi.SatuanDasarID AND ks.SatuanAsal = pi.SatuanID AND ks.NA = 'N' LIMIT 1), 1) FROM paket_item pi LEFT JOIN masteritem mi ON mi.ItemID = pi.ItemID AND mi.NA = 'N' LEFT JOIN departemen d ON d.DepartemenID = pi.DepartemenID AND d.NA = 'N' LEFT JOIN satuan s ON s.SatuanID = pi.SatuanID WHERE pi.PaketID = ? AND pi.NA = 'N' ORDER BY pi.PaketItemID`, id) if err != nil { return nil, err } defer rows.Close() result := make([]PaketItem, 0) for rows.Next() { var item PaketItem var namaItem, namaDepartemen, namaSatuan sql.NullString var harga, satuanID, satuanDasarID string var konversi float64 if err := rows.Scan( &item.PaketItemID, &item.ItemID, &namaItem, &namaDepartemen, &item.Jumlah, &namaSatuan, &harga, &item.Diskon, &item.TipeDiskon, &satuanID, &satuanDasarID, &konversi, ); err != nil { return nil, err } item.NamaItem = nullStringPtr(namaItem) item.NamaDepartemen = nullStringPtr(namaDepartemen) item.NamaSatuan = nullStringPtr(namaSatuan) hargaValue := parseDecimal(harga) diskon := parseDecimal(item.Diskon) nDisc := diskon if item.TipeDiskon == "Percentage" { nDisc = hargaValue * (diskon / 100) } if satuanDasarID == satuanID { konversi = 1 } item.Harga = phpNumberFormat(hargaValue, 2) item.SubTotal = phpNumberFormat((hargaValue-nDisc)*parseDecimal(item.Jumlah)*konversi, 2) result = append(result, item) } return result, rows.Err() } func (r MySQLLayananRepository) paketKamar(ctx context.Context, id string) ([]PaketKamar, error) { rows, err := r.db.QueryContext(ctx, ` SELECT pt.PaketTTID, d.Nama, k.Nama, CAST(pt.Jumlah AS CHAR), CAST(pt.Harga AS CHAR), pt.TipeDiskon, CAST(pt.Diskon AS CHAR) FROM paket_tt pt LEFT JOIN departemen d ON d.DepartemenID = pt.DepartemenID AND d.NA = 'N' LEFT JOIN kelas k ON k.KelasID = pt.KelasID AND k.NA = 'N' WHERE pt.PaketID = ? ORDER BY pt.PaketTTID`, id) if err != nil { return nil, err } defer rows.Close() result := make([]PaketKamar, 0) for rows.Next() { var item PaketKamar var namaDepartemen, namaKelas sql.NullString var harga, diskon string if err := rows.Scan( &item.PaketTTID, &namaDepartemen, &namaKelas, &item.Jumlah, &harga, &item.TipeDiskon, &diskon, ); err != nil { return nil, err } item.NamaDepartemen = nullStringPtr(namaDepartemen) item.NamaKelas = nullStringPtr(namaKelas) hargaValue := parseDecimal(harga) diskonValue := parseDecimal(diskon) item.Diskon = diskonValue if item.TipeDiskon == "Percentage" { item.Diskon = hargaValue * (diskonValue / 100) } item.Harga = phpNumberFormat(hargaValue, 2) item.SubTotal = phpNumberFormat((hargaValue-item.Diskon)*parseDecimal(item.Jumlah), 0) result = append(result, item) } return result, rows.Err() } func parseDecimal(value string) float64 { parsed, _ := strconv.ParseFloat(value, 64) return parsed } func phpNumberFormat(value float64, decimals int) string { power := math.Pow10(decimals) value = math.Round(value*power) / power formatted := strconv.FormatFloat(value, 'f', decimals, 64) parts := strings.SplitN(formatted, ".", 2) integer := parts[0] sign := "" if strings.HasPrefix(integer, "-") { sign = "-" integer = strings.TrimPrefix(integer, "-") } for index := len(integer) - 3; index > 0; index -= 3 { integer = integer[:index] + "," + integer[index:] } if len(parts) == 2 { return sign + integer + "." + parts[1] } return sign + integer } func laravelDateJSON(value string) string { if strings.HasPrefix(value, "0000-00-00") { return "-000001-11-29T16:52:48.000000Z" } location, err := time.LoadLocation("Asia/Jakarta") if err != nil { return value } parsed, err := time.ParseInLocation("2006-01-02 15:04:05", value, location) if err != nil { return value } return parsed.UTC().Format("2006-01-02T15:04:05.000000Z") }