diff --git a/svc/main.go b/svc/main.go index 1c9f3b7..593a191 100644 --- a/svc/main.go +++ b/svc/main.go @@ -431,6 +431,30 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router wrapV3(http.HandlerFunc(routes.GetCustomerBalanceListHandler)), ) + bindV3(r, pgDB, + "/api/finance/customer-balances/export-pdf", "GET", + "finance", "export", + wrapV3(routes.ExportCustomerBalancePDFHandler(mssql)), + ) + + bindV3(r, pgDB, + "/api/finance/customer-balances/export-excel", "GET", + "finance", "export", + wrapV3(routes.ExportCustomerBalanceExcelHandler(mssql)), + ) + + bindV3(r, pgDB, + "/api/finance/account-aging-statement", "GET", + "finance", "view", + wrapV3(http.HandlerFunc(routes.GetStatementAgingHandler)), + ) + + bindV3(r, pgDB, + "/api/finance/account-aging-statement/export-pdf", "GET", + "finance", "export", + wrapV3(routes.ExportStatementAgingPDFHandler(mssql)), + ) + // ============================================================ // REPORT (STATEMENTS) // ============================================================ diff --git a/svc/models/customer_balance_list.go b/svc/models/customer_balance_list.go index 75e67d0..f61ef4b 100644 --- a/svc/models/customer_balance_list.go +++ b/svc/models/customer_balance_list.go @@ -9,28 +9,36 @@ type CustomerBalanceListParams struct { RiskDurumu string IslemTipi string Ulke string + Il string + Ilce string } type CustomerBalanceListRow struct { - CariIlkGrup string `json:"cari_ilk_grup"` - Piyasa string `json:"piyasa"` - Temsilci string `json:"temsilci"` - Sirket string `json:"sirket"` - AnaCariKodu string `json:"ana_cari_kodu"` - AnaCariAdi string `json:"ana_cari_adi"` - CariKodu string `json:"cari_kodu"` - CariDetay string `json:"cari_detay"` - Ozellik03 string `json:"ozellik03"` - Ozellik05 string `json:"ozellik05"` - Ozellik06 string `json:"ozellik06"` - Ozellik07 string `json:"ozellik07"` - CariDoviz string `json:"cari_doviz"` - Bakiye12 float64 `json:"bakiye_1_2"` - TLBakiye12 float64 `json:"tl_bakiye_1_2"` - USDBakiye12 float64 `json:"usd_bakiye_1_2"` - Bakiye13 float64 `json:"bakiye_1_3"` - TLBakiye13 float64 `json:"tl_bakiye_1_3"` - USDBakiye13 float64 `json:"usd_bakiye_1_3"` - HesapAlinmayanGun NullInt32 `json:"hesap_alinmayan_gun"` - KalanFaturaOrtalamaVadeTarihi NullString `json:"kalan_fatura_ortalama_vade_tarihi"` + CariIlkGrup string `json:"cari_ilk_grup"` + Piyasa string `json:"piyasa"` + Temsilci string `json:"temsilci"` + Sirket string `json:"sirket"` + AnaCariKodu string `json:"ana_cari_kodu"` + AnaCariAdi string `json:"ana_cari_adi"` + CariKodu string `json:"cari_kodu"` + CariDetay string `json:"cari_detay"` + CariTip string `json:"cari_tip"` + Kanal1 string `json:"kanal_1"` + Ozellik03 string `json:"ozellik03"` + Ozellik05 string `json:"ozellik05"` + Ozellik06 string `json:"ozellik06"` + Ozellik07 string `json:"ozellik07"` + Il string `json:"il"` + Ilce string `json:"ilce"` + MuhasebeKodu string `json:"muhasebe_kodu"` + TC string `json:"tc"` + RiskDurumu string `json:"risk_durumu"` + SirketDetay string `json:"sirket_detay"` + CariDoviz string `json:"cari_doviz"` + Bakiye12 float64 `json:"bakiye_1_2"` + TLBakiye12 float64 `json:"tl_bakiye_1_2"` + USDBakiye12 float64 `json:"usd_bakiye_1_2"` + Bakiye13 float64 `json:"bakiye_1_3"` + TLBakiye13 float64 `json:"tl_bakiye_1_3"` + USDBakiye13 float64 `json:"usd_bakiye_1_3"` } diff --git a/svc/models/statement_aging_params.go b/svc/models/statement_aging_params.go new file mode 100644 index 0000000..130cdd7 --- /dev/null +++ b/svc/models/statement_aging_params.go @@ -0,0 +1,7 @@ +package models + +type StatementAgingParams struct { + AccountCode string `json:"accountcode"` + EndDate string `json:"enddate"` + Parislemler []string `json:"parislemler"` +} diff --git a/svc/queries/account_balance_fast.go b/svc/queries/account_balance_fast.go deleted file mode 100644 index 4152f9c..0000000 --- a/svc/queries/account_balance_fast.go +++ /dev/null @@ -1,91 +0,0 @@ -package queries - -import ( - "bssapp-backend/models" - "context" -) - -func GetCustomerBalanceList( - ctx context.Context, - params models.CustomerBalanceListParams, -) ([]models.CustomerBalanceListRow, error) { - - //------------------------------------------------ - // 1️⃣ DATA ÇEK - //------------------------------------------------ - - balances, err := getFastBalances(ctx, params.SelectedDate) - if err != nil { - return nil, err - } - - masterMap, err := getCariMasterMap(ctx) - if err != nil { - return nil, err - } - - //------------------------------------------------ - // 2️⃣ MERGE - //------------------------------------------------ - - resultMap := make(map[string]*models.CustomerBalanceListRow) - - for _, b := range balances { - - key := b.CariKodu + "|" + b.CariDoviz - - r, ok := resultMap[key] - if !ok { - - r = &models.CustomerBalanceListRow{ - CariKodu: b.CariKodu, - CariDoviz: b.CariDoviz, - } - - // Master - if m, ok := masterMap[b.CariKodu]; ok { - - r.CariDetay = m.CariDetay - r.Piyasa = m.Piyasa - r.Temsilci = m.Temsilci - - r.Ozellik03 = m.Ozellik03 - r.Ozellik05 = m.Ozellik05 - r.Ozellik06 = m.Ozellik06 - r.Ozellik07 = m.Ozellik07 - r.CariIlkGrup = m.Ozellik08 - } - - resultMap[key] = r - } - - //------------------------------------------------ - // 3️⃣ TOPLA - //------------------------------------------------ - - if b.PislemTipi == "1_2" { - - r.Bakiye12 += b.Bakiye - r.TLBakiye12 += b.KurBakiye - r.USDBakiye12 += b.KurBakiye / b.UsdKur - - } else if b.PislemTipi == "1_3" { - - r.Bakiye13 += b.Bakiye - r.TLBakiye13 += b.KurBakiye - r.USDBakiye13 += b.KurBakiye / b.UsdKur - } - } - - //------------------------------------------------ - // 4️⃣ SLICE DÖN - //------------------------------------------------ - - out := make([]models.CustomerBalanceListRow, 0, len(resultMap)) - - for _, v := range resultMap { - out = append(out, *v) - } - - return out, nil -} diff --git a/svc/queries/account_master.go b/svc/queries/account_master.go deleted file mode 100644 index fee80e0..0000000 --- a/svc/queries/account_master.go +++ /dev/null @@ -1,79 +0,0 @@ -package queries - -import ( - "bssapp-backend/db" - "context" -) - -type CariMasterRow struct { - CariKodu string - CariDetay string - Piyasa string - Temsilci string - Ozellik03 string - Ozellik05 string - Ozellik06 string - Ozellik07 string - Ozellik08 string -} - -func getCariMasterMap( - ctx context.Context, -) (map[string]CariMasterRow, error) { - - const q = ` - WITH CTE AS ( - SELECT - *, - rn = ROW_NUMBER() OVER ( - PARTITION BY LEFT(CariKodu,8) - ORDER BY CariKodu - ) - FROM dbo.MK_CARI_ILETISIM WITH(NOLOCK) - ) - SELECT - CariKodu, - CariDetay, - PIYASA, - CARI_TEMSILCI, - Ozellik03, - Ozellik05, - Ozellik06, - Ozellik07, - Ozellik08 - FROM CTE - WHERE rn=1 - ` - - rows, err := db.MssqlDB.QueryContext(ctx, q) - if err != nil { - return nil, err - } - defer rows.Close() - - out := make(map[string]CariMasterRow, 4096) - - for rows.Next() { - - var r CariMasterRow - - err := rows.Scan( - &r.CariKodu, - &r.CariDetay, - &r.Piyasa, - &r.Temsilci, - &r.Ozellik03, - &r.Ozellik05, - &r.Ozellik06, - &r.Ozellik07, - &r.Ozellik08, - ) - if err != nil { - return nil, err - } - - out[r.CariKodu] = r - } - - return out, nil -} diff --git a/svc/queries/currency_cache.go b/svc/queries/currency_cache.go index 3859591..5a31958 100644 --- a/svc/queries/currency_cache.go +++ b/svc/queries/currency_cache.go @@ -3,62 +3,10 @@ package queries import ( "bssapp-backend/models" "database/sql" - "sync" - "time" + "strings" ) -/* =============================== - CACHE STRUCT -================================ */ - -type currencyCacheItem struct { - data *models.TodayCurrencyV3 - expiresAt time.Time -} - -var ( - currencyCache = make(map[string]currencyCacheItem) - cacheMutex sync.RWMutex - cacheTTL = 5 * time.Minute -) - -/* =============================== - MAIN CACHE FUNC -================================ */ - -func GetCachedCurrencyV3(db *sql.DB, code string) (*models.TodayCurrencyV3, error) { - - now := time.Now() - - /* ---------- READ CACHE ---------- */ - cacheMutex.RLock() - - item, ok := currencyCache[code] - - if ok && now.Before(item.expiresAt) { - cacheMutex.RUnlock() - return item.data, nil - } - - cacheMutex.RUnlock() - - /* ---------- FETCH DB ---------- */ - - data, err := GetTodayCurrencyV3(db, code) - if err != nil { - return nil, err - } - - /* ---------- WRITE CACHE ---------- */ - - cacheMutex.Lock() - - currencyCache[code] = currencyCacheItem{ - data: data, - expiresAt: now.Add(cacheTTL), - } - - cacheMutex.Unlock() - - return data, nil +// GetCachedCurrencyV3 keeps compatibility with existing order routes. +func GetCachedCurrencyV3(db *sql.DB, currencyCode string) (*models.TodayCurrencyV3, error) { + return GetTodayCurrencyV3(db, strings.ToUpper(strings.TrimSpace(currencyCode))) } diff --git a/svc/queries/customer_balance_list.go b/svc/queries/customer_balance_list.go new file mode 100644 index 0000000..82433f8 --- /dev/null +++ b/svc/queries/customer_balance_list.go @@ -0,0 +1,784 @@ +package queries + +import ( + "bssapp-backend/auth" + "bssapp-backend/db" + "bssapp-backend/internal/authz" + "bssapp-backend/models" + "context" + "database/sql" + "fmt" + "log" + "sort" + "strconv" + "strings" +) + +type mkCariBakiyeLine struct { + CurrAccTypeCode int + CariKodu string + CariDoviz string + SirketKodu int + PislemTipi string + YerelBakiye float64 + Bakiye float64 +} + +type cariMeta struct { + CariDetay string + CariTip string + Kanal1 string + Piyasa string + Temsilci string + Ulke string + Il string + Ilce string + TC string + RiskDurumu string + MuhasebeKodu string + SirketDetay string +} + +type masterCariMeta struct { + CariDetay string + Kanal1 string + Piyasa string + Temsilci string + Ulke string + Il string + Ilce string + RiskDurumu string +} + +type balanceFilters struct { + cariIlkGrup map[string]struct{} + piyasa map[string]struct{} + temsilci map[string]struct{} + riskDurumu map[string]struct{} + islemTipi map[string]struct{} + ulke map[string]struct{} + il map[string]struct{} + ilce map[string]struct{} +} + +func GetCustomerBalanceList(ctx context.Context, params models.CustomerBalanceListParams) ([]models.CustomerBalanceListRow, error) { + if strings.TrimSpace(params.SelectedDate) == "" { + return nil, fmt.Errorf("selected_date is required") + } + + lines, err := loadBalanceLines(ctx, params.SelectedDate, params.CariSearch) + if err != nil { + return nil, err + } + + metaMap, err := loadCariMetaMap(ctx, lines) + if err != nil { + log.Printf("customer_balance_list: cari meta query failed, fallback without meta: %v", err) + metaMap = map[string]cariMeta{} + } + + masterMetaMap, err := loadMasterCariMetaMap(ctx, lines) + if err != nil { + log.Printf("customer_balance_list: master cari meta query failed, fallback without master meta: %v", err) + masterMetaMap = map[string]masterCariMeta{} + } + + companyMap, err := loadCompanyMap(ctx) + if err != nil { + return nil, err + } + + glMap, err := loadGLAccountMap(ctx, lines) + if err != nil { + return nil, err + } + + rateMap, err := loadNearestTryRates(ctx) + if err != nil { + return nil, err + } + + usdTry := rateMap["USD"] + if usdTry <= 0 { + usdTry = 1 + } + + filters := buildFilters(params) + agg := make(map[string]*models.CustomerBalanceListRow, len(lines)) + + for _, ln := range lines { + cari := strings.TrimSpace(ln.CariKodu) + if cari == "" { + continue + } + + curr := strings.ToUpper(strings.TrimSpace(ln.CariDoviz)) + if curr == "" { + curr = "TRY" + } + + meta := metaMap[metaKey(ln.CurrAccTypeCode, cari)] + meta.MuhasebeKodu = glMap[glKey(ln.CurrAccTypeCode, cari, ln.SirketKodu)] + meta.SirketDetay = companyMap[ln.SirketKodu] + master := deriveMasterCari(cari) + mm := masterMetaMap[master] + + if strings.TrimSpace(mm.Kanal1) != "" { + meta.Kanal1 = mm.Kanal1 + } + if strings.TrimSpace(mm.Piyasa) != "" { + meta.Piyasa = mm.Piyasa + } + if strings.TrimSpace(mm.Temsilci) != "" { + meta.Temsilci = mm.Temsilci + } + if strings.TrimSpace(mm.Ulke) != "" { + meta.Ulke = mm.Ulke + } + if strings.TrimSpace(mm.Il) != "" { + meta.Il = mm.Il + } + if strings.TrimSpace(mm.Ilce) != "" { + meta.Ilce = mm.Ilce + } + if strings.TrimSpace(mm.RiskDurumu) != "" { + meta.RiskDurumu = mm.RiskDurumu + } + + if !filters.matchLine(ln.PislemTipi, meta) { + continue + } + + key := strconv.Itoa(ln.CurrAccTypeCode) + "|" + cari + "|" + curr + "|" + strconv.Itoa(ln.SirketKodu) + row, ok := agg[key] + if !ok { + row = &models.CustomerBalanceListRow{ + CariIlkGrup: meta.Kanal1, + Piyasa: meta.Piyasa, + Temsilci: meta.Temsilci, + Sirket: strconv.Itoa(ln.SirketKodu), + AnaCariKodu: master, + AnaCariAdi: firstNonEmpty(mm.CariDetay, meta.CariDetay), + CariKodu: cari, + CariDetay: meta.CariDetay, + CariTip: meta.CariTip, + Kanal1: meta.Kanal1, + Ozellik03: meta.RiskDurumu, + Ozellik05: meta.Ulke, + Ozellik06: meta.Il, + Ozellik07: meta.Ilce, + Il: meta.Il, + Ilce: meta.Ilce, + MuhasebeKodu: meta.MuhasebeKodu, + TC: meta.TC, + RiskDurumu: meta.RiskDurumu, + SirketDetay: meta.SirketDetay, + CariDoviz: curr, + } + agg[key] = row + } + + usd := toUSD(ln.Bakiye, curr, usdTry, rateMap) + + switch strings.TrimSpace(ln.PislemTipi) { + case "1_2": + row.Bakiye12 += ln.Bakiye + row.TLBakiye12 += ln.YerelBakiye + row.USDBakiye12 += usd + case "1_3": + row.Bakiye13 += ln.Bakiye + row.TLBakiye13 += ln.YerelBakiye + row.USDBakiye13 += usd + } + } + + out := make([]models.CustomerBalanceListRow, 0, len(agg)) + for _, v := range agg { + out = append(out, *v) + } + + sort.Slice(out, func(i, j int) bool { + if out[i].AnaCariKodu == out[j].AnaCariKodu { + if out[i].CariKodu == out[j].CariKodu { + return out[i].CariDoviz < out[j].CariDoviz + } + return out[i].CariKodu < out[j].CariKodu + } + return out[i].AnaCariKodu < out[j].AnaCariKodu + }) + + return out, nil +} + +func loadMasterCariMetaMap(ctx context.Context, lines []mkCariBakiyeLine) (map[string]masterCariMeta, error) { + masters := make(map[string]struct{}) + for _, ln := range lines { + m := strings.TrimSpace(deriveMasterCari(ln.CariKodu)) + if m != "" { + masters[m] = struct{}{} + } + } + if len(masters) == 0 { + return map[string]masterCariMeta{}, nil + } + + query := fmt.Sprintf(` +WITH BaseCari AS +( + SELECT + CB.CurrAccCode, + CB.CurrAccTypeCode, + MasterCari = LEFT(CB.CurrAccCode, 8), + rn = ROW_NUMBER() OVER + ( + PARTITION BY LEFT(CB.CurrAccCode, 8) + ORDER BY CB.CurrAccCode + ) + FROM cdCurrAcc CB WITH (NOLOCK) + WHERE CB.CurrAccTypeCode IN (1,3) + AND LEFT(CB.CurrAccCode, 8) IN (%s) +), +FirstCari AS +( + SELECT * + FROM BaseCari + WHERE rn = 1 +) +SELECT + CariKodu = F.MasterCari, + CariDetay = ISNULL(cd.CurrAccDescription, ''), + KANAL_1 = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt08Desc ELSE CDesc.CustomerAtt08Desc END, ''), + PIYASA = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt01Desc ELSE CDesc.CustomerAtt01Desc END, ''), + CARI_TEMSILCI = ISNULL( + CASE + WHEN ISNULL(CASE WHEN F.CurrAccTypeCode = 1 THEN VDesc.VendorAtt02Desc ELSE CDesc.CustomerAtt02Desc END,'') = '' + THEN ISNULL(CASE WHEN F.CurrAccTypeCode = 1 THEN VAttr.VendorAtt09 ELSE CAttr.CustomerAtt09 END,'') + ELSE CASE WHEN F.CurrAccTypeCode = 1 THEN VDesc.VendorAtt02Desc ELSE CDesc.CustomerAtt02Desc END + END,'' + ), + ULKE = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt05Desc ELSE CDesc.CustomerAtt05Desc END, ''), + IL = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt06Desc ELSE CDesc.CustomerAtt06Desc END, ''), + ILCE = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt07Desc ELSE CDesc.CustomerAtt07Desc END, ''), + Risk_Durumu = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt03Desc ELSE CDesc.CustomerAtt03Desc END, '') +FROM FirstCari F +LEFT JOIN cdCurrAccDesc cd WITH (NOLOCK) + ON cd.CurrAccTypeCode = F.CurrAccTypeCode + AND cd.CurrAccCode = F.CurrAccCode + AND cd.LangCode = 'TR' +LEFT JOIN VendorAttributeDescriptions('TR') VDesc + ON VDesc.CurrAccCode = F.CurrAccCode + AND VDesc.CurrAccTypeCode = F.CurrAccTypeCode +LEFT JOIN CustomerAttributeDescriptions('TR') CDesc + ON CDesc.CurrAccCode = F.CurrAccCode + AND CDesc.CurrAccTypeCode = F.CurrAccTypeCode +LEFT JOIN VendorAttributes VAttr + ON VAttr.CurrAccCode = F.CurrAccCode + AND VAttr.CurrAccTypeCode = F.CurrAccTypeCode +LEFT JOIN CustomerAttributes CAttr + ON CAttr.CurrAccCode = F.CurrAccCode + AND CAttr.CurrAccTypeCode = F.CurrAccTypeCode +ORDER BY F.MasterCari; +`, quotedInList(masters)) + + rows, err := db.MssqlDB.QueryContext(ctx, query) + if err != nil { + return nil, fmt.Errorf("master cari meta query error: %w", err) + } + defer rows.Close() + + out := make(map[string]masterCariMeta, len(masters)) + for rows.Next() { + var master string + var m masterCariMeta + if err := rows.Scan( + &master, + &m.CariDetay, + &m.Kanal1, + &m.Piyasa, + &m.Temsilci, + &m.Ulke, + &m.Il, + &m.Ilce, + &m.RiskDurumu, + ); err != nil { + return nil, err + } + out[strings.TrimSpace(master)] = m + } + if err := rows.Err(); err != nil { + return nil, err + } + return out, nil +} + +func loadBalanceLines(ctx context.Context, selectedDate, cariSearch string) ([]mkCariBakiyeLine, error) { + query := ` + SELECT + CurrAccTypeCode, + CariKodu, + CariDoviz, + SirketKodu, + PislemTipi, + YerelBakiye, + Bakiye + FROM dbo.MK_CARI_BAKIYE_LIST(@SonTarih) + WHERE (@CariSearch = '' OR CariKodu LIKE '%' + @CariSearch + '%') + ` + + rows, err := db.MssqlDB.QueryContext(ctx, query, + sql.Named("SonTarih", selectedDate), + sql.Named("CariSearch", strings.TrimSpace(cariSearch)), + ) + if err != nil { + return nil, fmt.Errorf("MK_CARI_BAKIYE_LIST query error: %w", err) + } + defer rows.Close() + + out := make([]mkCariBakiyeLine, 0, 4096) + for rows.Next() { + var r mkCariBakiyeLine + if err := rows.Scan( + &r.CurrAccTypeCode, + &r.CariKodu, + &r.CariDoviz, + &r.SirketKodu, + &r.PislemTipi, + &r.YerelBakiye, + &r.Bakiye, + ); err != nil { + return nil, err + } + out = append(out, r) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return out, nil +} + +func loadCariMetaMap(ctx context.Context, lines []mkCariBakiyeLine) (map[string]cariMeta, error) { + vendorCodes := make(map[string]struct{}) + customerCodes := make(map[string]struct{}) + + for _, ln := range lines { + code := strings.TrimSpace(ln.CariKodu) + if code == "" { + continue + } + if ln.CurrAccTypeCode == 1 { + vendorCodes[code] = struct{}{} + } else if ln.CurrAccTypeCode == 3 { + customerCodes[code] = struct{}{} + } + } + + if len(vendorCodes) == 0 && len(customerCodes) == 0 { + return map[string]cariMeta{}, nil + } + + whereParts := make([]string, 0, 2) + if len(vendorCodes) > 0 { + whereParts = append(whereParts, fmt.Sprintf("(c.CurrAccTypeCode=1 AND c.CurrAccCode IN (%s))", quotedInList(vendorCodes))) + } + if len(customerCodes) > 0 { + whereParts = append(whereParts, fmt.Sprintf("(c.CurrAccTypeCode=3 AND c.CurrAccCode IN (%s))", quotedInList(customerCodes))) + } + + query := fmt.Sprintf(` + SELECT + c.CurrAccTypeCode, + c.CurrAccCode, + CariDetay = ISNULL(d.CurrAccDescription, ''), + CariTip = CASE WHEN c.CurrAccTypeCode = 1 THEN N'Satıcı' ELSE N'Müşteri' END, + KANAL_1 = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt08Desc ELSE cad.CustomerAtt08Desc END, ''), + PIYASA = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt01Desc ELSE cad.CustomerAtt01Desc END, ''), + CARI_TEMSILCI = ISNULL( + CASE + WHEN ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt02Desc ELSE cad.CustomerAtt02Desc END, '') = '' + THEN ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN va.VendorAtt09 ELSE ca.CustomerAtt09 END, '') + ELSE CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt02Desc ELSE cad.CustomerAtt02Desc END + END, + ''), + ULKE = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt05Desc ELSE cad.CustomerAtt05Desc END, ''), + IL = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt06Desc ELSE cad.CustomerAtt06Desc END, ''), + ILCE = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt07Desc ELSE cad.CustomerAtt07Desc END, ''), + TC = ISNULL(c.IdentityNum, ''), + Risk_Durumu = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt03Desc ELSE cad.CustomerAtt03Desc END, '') + FROM cdCurrAcc c WITH(NOLOCK) + LEFT JOIN cdCurrAccDesc d WITH(NOLOCK) + ON d.CurrAccTypeCode = c.CurrAccTypeCode + AND d.CurrAccCode = c.CurrAccCode + AND d.LangCode = 'TR' + LEFT JOIN VendorAttributes va WITH(NOLOCK) + ON va.CurrAccTypeCode = c.CurrAccTypeCode + AND va.CurrAccCode = c.CurrAccCode + LEFT JOIN VendorAttributeDescriptions('TR') vad + ON vad.CurrAccTypeCode = c.CurrAccTypeCode + AND vad.CurrAccCode = c.CurrAccCode + LEFT JOIN CustomerAttributes ca WITH(NOLOCK) + ON ca.CurrAccTypeCode = c.CurrAccTypeCode + AND ca.CurrAccCode = c.CurrAccCode + LEFT JOIN CustomerAttributeDescriptions('TR') cad + ON cad.CurrAccTypeCode = c.CurrAccTypeCode + AND cad.CurrAccCode = c.CurrAccCode + WHERE c.CurrAccTypeCode IN (1,3) + AND (%s) + `, strings.Join(whereParts, " OR ")) + + rows, err := db.MssqlDB.QueryContext(ctx, query) + if err != nil { + return nil, fmt.Errorf("cari meta query error: %w", err) + } + defer rows.Close() + + out := make(map[string]cariMeta, len(lines)) + for rows.Next() { + var t int + var code string + var m cariMeta + if err := rows.Scan( + &t, + &code, + &m.CariDetay, + &m.CariTip, + &m.Kanal1, + &m.Piyasa, + &m.Temsilci, + &m.Ulke, + &m.Il, + &m.Ilce, + &m.TC, + &m.RiskDurumu, + ); err != nil { + return nil, err + } + out[metaKey(t, code)] = m + } + if err := rows.Err(); err != nil { + return nil, err + } + + return out, nil +} + +func loadGLAccountMap(ctx context.Context, lines []mkCariBakiyeLine) (map[string]string, error) { + vendorCodes := make(map[string]struct{}) + customerCodes := make(map[string]struct{}) + companyCodes := make(map[int]struct{}) + + for _, ln := range lines { + code := strings.TrimSpace(ln.CariKodu) + if code == "" { + continue + } + companyCodes[ln.SirketKodu] = struct{}{} + if ln.CurrAccTypeCode == 1 { + vendorCodes[code] = struct{}{} + } else if ln.CurrAccTypeCode == 3 { + customerCodes[code] = struct{}{} + } + } + + if len(companyCodes) == 0 || (len(vendorCodes) == 0 && len(customerCodes) == 0) { + return map[string]string{}, nil + } + + whereParts := make([]string, 0, 2) + if len(vendorCodes) > 0 { + whereParts = append(whereParts, fmt.Sprintf("(CurrAccTypeCode=1 AND CurrAccCode IN (%s))", quotedInList(vendorCodes))) + } + if len(customerCodes) > 0 { + whereParts = append(whereParts, fmt.Sprintf("(CurrAccTypeCode=3 AND CurrAccCode IN (%s))", quotedInList(customerCodes))) + } + + query := fmt.Sprintf(` + SELECT CurrAccTypeCode, CurrAccCode, CompanyCode, GLAccCode + FROM prCurrAccGLAccount WITH(NOLOCK) + WHERE PostAccTypeCode = 100 + AND CompanyCode IN (%s) + AND (%s) + `, intInList(companyCodes), strings.Join(whereParts, " OR ")) + + rows, err := db.MssqlDB.QueryContext(ctx, query) + if err != nil { + return nil, fmt.Errorf("gl account query error: %w", err) + } + defer rows.Close() + + out := make(map[string]string) + for rows.Next() { + var t int + var code string + var company int + var gl sql.NullString + if err := rows.Scan(&t, &code, &company, &gl); err != nil { + return nil, err + } + out[glKey(t, code, company)] = strings.TrimSpace(gl.String) + } + if err := rows.Err(); err != nil { + return nil, err + } + + return out, nil +} + +func loadCompanyMap(ctx context.Context) (map[int]string, error) { + rows, err := db.MssqlDB.QueryContext(ctx, `SELECT CompanyCode, CompanyName FROM cdCompany WITH(NOLOCK)`) + if err != nil { + return nil, fmt.Errorf("company map query error: %w", err) + } + defer rows.Close() + + out := make(map[int]string) + for rows.Next() { + var code int + var name sql.NullString + if err := rows.Scan(&code, &name); err != nil { + return nil, err + } + out[code] = strings.TrimSpace(name.String) + } + if err := rows.Err(); err != nil { + return nil, err + } + return out, nil +} + +func loadNearestTryRates(ctx context.Context) (map[string]float64, error) { + query := ` + WITH Ranked AS ( + SELECT + CurrencyCode, + Rate, + rn = ROW_NUMBER() OVER ( + PARTITION BY CurrencyCode + ORDER BY ABS(DATEDIFF(DAY, Date, GETDATE())), Date DESC + ) + FROM AllExchangeRates + WHERE RelationCurrencyCode = 'TRY' + AND ExchangeTypeCode = 6 + AND Rate > 0 + ) + SELECT CurrencyCode, Rate + FROM Ranked + WHERE rn = 1 + ` + + rows, err := db.MssqlDB.QueryContext(ctx, query) + if err != nil { + return nil, fmt.Errorf("exchange rates query error: %w", err) + } + defer rows.Close() + + out := map[string]float64{"TRY": 1} + for rows.Next() { + var code string + var rate float64 + if err := rows.Scan(&code, &rate); err != nil { + return nil, err + } + code = strings.ToUpper(strings.TrimSpace(code)) + if code != "" && rate > 0 { + out[code] = rate + } + } + if err := rows.Err(); err != nil { + return nil, err + } + + return out, nil +} + +func toUSD(amount float64, currency string, usdTry float64, rateMap map[string]float64) float64 { + if usdTry <= 0 { + return 0 + } + + switch currency { + case "USD": + return amount + case "TRY": + return amount / usdTry + default: + currTry := rateMap[currency] + if currTry <= 0 { + return 0 + } + return (amount * currTry) / usdTry + } +} + +func deriveMasterCari(cari string) string { + cari = strings.TrimSpace(cari) + if cari == "" { + return "" + } + + base := cari + if idx := strings.Index(base, "/"); idx > 0 { + base = base[:idx] + } + + base = strings.TrimSpace(base) + if len(base) >= 8 { + return strings.TrimSpace(base[:8]) + } + + return base +} + +func buildFilters(params models.CustomerBalanceListParams) balanceFilters { + return balanceFilters{ + cariIlkGrup: parseCSVSet(params.CariIlkGrup), + piyasa: parseCSVSet(params.Piyasa), + temsilci: parseCSVSet(params.Temsilci), + riskDurumu: parseCSVSet(params.RiskDurumu), + islemTipi: parseCSVSet(params.IslemTipi), + ulke: parseCSVSet(params.Ulke), + il: parseCSVSet(params.Il), + ilce: parseCSVSet(params.Ilce), + } +} + +func (f balanceFilters) matchLine(islemTipi string, m cariMeta) bool { + if !matchSet(f.islemTipi, islemTipi) { + return false + } + if !matchSet(f.cariIlkGrup, m.Kanal1) { + return false + } + if !matchSet(f.piyasa, m.Piyasa) { + return false + } + if !matchSet(f.temsilci, m.Temsilci) { + return false + } + if !matchSet(f.riskDurumu, m.RiskDurumu) { + return false + } + if !matchSet(f.ulke, m.Ulke) { + return false + } + if !matchSet(f.il, m.Il) { + return false + } + if !matchSet(f.ilce, m.Ilce) { + return false + } + return true +} + +func matchSet(set map[string]struct{}, value string) bool { + if len(set) == 0 { + return true + } + trimmed := strings.TrimSpace(value) + if trimmed == "" { + return true + } + _, ok := set[trimmed] + return ok +} + +func parseCSVSet(v string) map[string]struct{} { + out := make(map[string]struct{}) + for _, p := range strings.Split(v, ",") { + t := strings.TrimSpace(p) + if t == "" { + continue + } + out[t] = struct{}{} + } + return out +} + +func getAuthorizedPiyasaCodes(ctx context.Context) ([]string, error) { + claims, ok := auth.GetClaimsFromContext(ctx) + if !ok || claims == nil { + return nil, fmt.Errorf("unauthorized: claims not found") + } + if claims.IsAdmin() { + return nil, nil + } + + rawCodes := authz.GetPiyasaCodesFromCtx(ctx) + if len(rawCodes) == 0 { + return []string{}, nil + } + + unique := make(map[string]struct{}, len(rawCodes)) + out := make([]string, 0, len(rawCodes)) + for _, code := range rawCodes { + norm := strings.ToUpper(strings.TrimSpace(code)) + if norm == "" { + continue + } + if _, exists := unique[norm]; exists { + continue + } + unique[norm] = struct{}{} + out = append(out, norm) + } + if len(out) == 0 { + return []string{}, nil + } + return out, nil +} + +func buildPiyasaWhereClause(codes []string, column string) string { + if len(codes) == 0 { + return "1=1" + } + return authz.BuildINClause(column, codes) +} + +func metaKey(currType int, code string) string { + return strconv.Itoa(currType) + "|" + strings.TrimSpace(code) +} + +func glKey(currType int, code string, company int) string { + return strconv.Itoa(currType) + "|" + strings.TrimSpace(code) + "|" + strconv.Itoa(company) +} + +func quotedInList(set map[string]struct{}) string { + vals := make([]string, 0, len(set)) + for v := range set { + esc := strings.ReplaceAll(strings.TrimSpace(v), "'", "''") + if esc != "" { + vals = append(vals, "'"+esc+"'") + } + } + if len(vals) == 0 { + return "''" + } + sort.Strings(vals) + return strings.Join(vals, ",") +} + +func intInList(set map[int]struct{}) string { + vals := make([]int, 0, len(set)) + for v := range set { + vals = append(vals, v) + } + if len(vals) == 0 { + return "0" + } + sort.Ints(vals) + parts := make([]string, 0, len(vals)) + for _, v := range vals { + parts = append(parts, strconv.Itoa(v)) + } + return strings.Join(parts, ",") +} + +func firstNonEmpty(v ...string) string { + for _, s := range v { + if strings.TrimSpace(s) != "" { + return s + } + } + return "" +} diff --git a/svc/queries/fast_balance.go b/svc/queries/fast_balance.go deleted file mode 100644 index 871dd0a..0000000 --- a/svc/queries/fast_balance.go +++ /dev/null @@ -1,69 +0,0 @@ -package queries - -import ( - "bssapp-backend/db" - "context" - "database/sql" -) - -type FastBalanceRow struct { - CariKodu string - CariDoviz string - PislemTipi string - SirketKodu int - YerelBakiye float64 - Bakiye float64 - IslemTarihi string - IslemKur float64 - TLBakiye float64 - TLYerel float64 - GuncelKur float64 - KurBakiye float64 - UsdKur float64 -} - -func getFastBalances( - ctx context.Context, - date string, -) ([]FastBalanceRow, error) { - - rows, err := db.MssqlDB.QueryContext( - ctx, - `EXEC dbo.SP_CARI_BAKIYE_API_FAST @Tarih`, - sql.Named("Tarih", date), - ) - if err != nil { - return nil, err - } - defer rows.Close() - - out := make([]FastBalanceRow, 0, 4096) - - for rows.Next() { - - var r FastBalanceRow - - err := rows.Scan( - &r.CariKodu, - &r.CariDoviz, - &r.PislemTipi, - &r.SirketKodu, - &r.YerelBakiye, - &r.Bakiye, - &r.IslemTarihi, - &r.IslemKur, - &r.TLBakiye, - &r.TLYerel, - &r.GuncelKur, - &r.KurBakiye, - &r.UsdKur, - ) - if err != nil { - return nil, err - } - - out = append(out, r) - } - - return out, nil -} diff --git a/svc/queries/statement_aging.go b/svc/queries/statement_aging.go new file mode 100644 index 0000000..83c33fe --- /dev/null +++ b/svc/queries/statement_aging.go @@ -0,0 +1,414 @@ +package queries + +import ( + "bssapp-backend/db" + "bssapp-backend/models" + "context" + "database/sql" + "fmt" + "math" + "sort" + "strconv" + "strings" + "time" +) + +func GetStatementAging(params models.StatementAgingParams) ([]map[string]interface{}, error) { + accountCode := normalizeMasterAccountCode(params.AccountCode) + if strings.TrimSpace(accountCode) == "" { + return nil, fmt.Errorf("accountcode is required") + } + if strings.TrimSpace(params.EndDate) == "" { + return nil, fmt.Errorf("enddate is required") + } + + useType2, useType3 := resolveUseTypes(params.Parislemler) + endDate, _ := time.Parse("2006-01-02", strings.TrimSpace(params.EndDate)) + + rows, err := db.MssqlDB.Query(` + EXEC dbo.SP_FIFO_MATCH_FINAL + @Cari8 = @Cari8, + @SonTarih = @SonTarih, + @UseType2 = @UseType2, + @UseType3 = @UseType3; + `, + sql.Named("Cari8", accountCode), + sql.Named("SonTarih", params.EndDate), + sql.Named("UseType2", useType2), + sql.Named("UseType3", useType3), + ) + if err != nil { + return nil, fmt.Errorf("SP_FIFO_MATCH_FINAL query error: %w", err) + } + defer rows.Close() + + columns, err := rows.Columns() + if err != nil { + return nil, fmt.Errorf("columns read error: %w", err) + } + + result := make([]map[string]interface{}, 0, 2048) + cari8Set := make(map[string]struct{}) + currencySet := make(map[string]struct{}) + for rows.Next() { + values := make([]interface{}, len(columns)) + scanArgs := make([]interface{}, len(columns)) + for i := range values { + scanArgs[i] = &values[i] + } + + if err := rows.Scan(scanArgs...); err != nil { + return nil, fmt.Errorf("row scan error: %w", err) + } + + row := make(map[string]interface{}, len(columns)) + for i, col := range columns { + switch v := values[i].(type) { + case nil: + row[col] = nil + case []byte: + row[col] = string(v) + case time.Time: + row[col] = v.Format("2006-01-02") + default: + row[col] = v + } + } + + cari8 := strings.TrimSpace(asString(row["Cari8"])) + if cari8 != "" { + cari8Set[cari8] = struct{}{} + } + curr := strings.ToUpper(strings.TrimSpace(asString(row["DocCurrencyCode"]))) + if curr != "" && curr != "TRY" { + currencySet[curr] = struct{}{} + } + currencySet["USD"] = struct{}{} + + result = append(result, row) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows error: %w", err) + } + + cariDetailMap, err := loadAgingMasterCariDetailMap(context.Background(), cari8Set) + if err != nil { + return nil, err + } + rateSeriesByCurr, err := loadTryRateSeriesByCurrency(context.Background(), currencySet) + if err != nil { + return nil, err + } + + for i := range result { + row := result[i] + cari8 := strings.TrimSpace(asString(row["Cari8"])) + curr := strings.ToUpper(strings.TrimSpace(asString(row["DocCurrencyCode"]))) + if curr == "" { + curr = "TRY" + } + aciklama := strings.ToUpper(strings.TrimSpace(asString(row["Aciklama"]))) + targetDate := endDate + if aciklama != "ACIKKALEM" { + if odemeTarihi, ok := parseDateOnly(asString(row["OdemeTarihi"])); ok { + targetDate = odemeTarihi + } + } + + tutar := asFloat64(row["EslesenTutar"]) + currTry := resolveTryRate(curr, targetDate, rateSeriesByCurr) + usdTry := resolveTryRate("USD", targetDate, rateSeriesByCurr) + tryTutar := toTRYByRate(tutar, curr, currTry) + usdTutar := toUSDByRates(tutar, curr, currTry, usdTry) + gunKur := usdRateInCurrency(curr, currTry, usdTry) + + row["CariDetay"] = cariDetailMap[cari8] + row["UsdTutar"] = round2(usdTutar) + row["TryTutar"] = round2(tryTutar) + row["GunKur"] = round6(gunKur) + } + + return result, nil +} + +func resolveUseTypes(parislemler []string) (int, int) { + if len(parislemler) == 0 { + return 1, 0 + } + + useType2 := 0 + useType3 := 0 + + for _, v := range parislemler { + switch strings.TrimSpace(v) { + case "2": + useType2 = 1 + case "3": + useType3 = 1 + } + } + + if useType2 == 0 && useType3 == 0 { + return 1, 0 + } + + return useType2, useType3 +} + +func loadAgingMasterCariDetailMap(ctx context.Context, cari8Set map[string]struct{}) (map[string]string, error) { + if len(cari8Set) == 0 { + return map[string]string{}, nil + } + + query := fmt.Sprintf(` +WITH BaseCari AS ( + SELECT + CurrAccCode, + CurrAccTypeCode, + MasterCari = LEFT(CurrAccCode, 8), + rn = ROW_NUMBER() OVER ( + PARTITION BY LEFT(CurrAccCode, 8) + ORDER BY CurrAccCode + ) + FROM cdCurrAcc WITH (NOLOCK) + WHERE CurrAccTypeCode IN (1,3) + AND LEFT(CurrAccCode, 8) IN (%s) +) +SELECT + b.MasterCari, + CariDetay = ISNULL(d.CurrAccDescription, '') +FROM BaseCari b +LEFT JOIN cdCurrAccDesc d WITH (NOLOCK) + ON d.CurrAccTypeCode = b.CurrAccTypeCode + AND d.CurrAccCode = b.CurrAccCode + AND d.LangCode = 'TR' +WHERE b.rn = 1; +`, quotedInList(cari8Set)) + + rows, err := db.MssqlDB.QueryContext(ctx, query) + if err != nil { + return nil, fmt.Errorf("aging cari detail query error: %w", err) + } + defer rows.Close() + + out := make(map[string]string, len(cari8Set)) + for rows.Next() { + var cari8 string + var detail sql.NullString + if err := rows.Scan(&cari8, &detail); err != nil { + return nil, err + } + out[strings.TrimSpace(cari8)] = strings.TrimSpace(detail.String) + } + if err := rows.Err(); err != nil { + return nil, err + } + return out, nil +} + +func asString(v interface{}) string { + switch x := v.(type) { + case nil: + return "" + case string: + return x + case []byte: + return string(x) + default: + return fmt.Sprint(x) + } +} + +func asFloat64(v interface{}) float64 { + switch x := v.(type) { + case nil: + return 0 + case float64: + return x + case float32: + return float64(x) + case int64: + return float64(x) + case int32: + return float64(x) + case int: + return float64(x) + case string: + return parseNumberString(x) + case []byte: + return parseNumberString(string(x)) + default: + return parseNumberString(fmt.Sprint(x)) + } +} + +func parseNumberString(s string) float64 { + s = strings.TrimSpace(s) + if s == "" { + return 0 + } + + hasComma := strings.Contains(s, ",") + hasDot := strings.Contains(s, ".") + if hasComma && hasDot { + if strings.LastIndex(s, ",") > strings.LastIndex(s, ".") { + s = strings.ReplaceAll(s, ".", "") + s = strings.Replace(s, ",", ".", 1) + } else { + s = strings.ReplaceAll(s, ",", "") + } + } else if hasComma { + s = strings.ReplaceAll(s, ".", "") + s = strings.Replace(s, ",", ".", 1) + } + + n, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0 + } + return n +} + +func round2(v float64) float64 { + return math.Round(v*100) / 100 +} + +func round6(v float64) float64 { + return math.Round(v*1_000_000) / 1_000_000 +} + +type ratePoint struct { + date time.Time + rate float64 +} + +func loadTryRateSeriesByCurrency(ctx context.Context, currencies map[string]struct{}) (map[string][]ratePoint, error) { + if len(currencies) == 0 { + return map[string][]ratePoint{}, nil + } + + query := fmt.Sprintf(` +SELECT CurrencyCode, Rate, CAST([Date] AS date) AS RateDate +FROM AllExchangeRates +WHERE RelationCurrencyCode = 'TRY' + AND ExchangeTypeCode = 6 + AND Rate > 0 + AND CurrencyCode IN (%s) +`, quotedInList(currencies)) + + rows, err := db.MssqlDB.QueryContext(ctx, query) + if err != nil { + return nil, fmt.Errorf("aging currency series query error: %w", err) + } + defer rows.Close() + + out := make(map[string][]ratePoint, len(currencies)) + for rows.Next() { + var code string + var rate float64 + var dt time.Time + if err := rows.Scan(&code, &rate, &dt); err != nil { + return nil, err + } + code = strings.ToUpper(strings.TrimSpace(code)) + out[code] = append(out[code], ratePoint{date: dt, rate: rate}) + } + if err := rows.Err(); err != nil { + return nil, err + } + + for c := range out { + sort.Slice(out[c], func(i, j int) bool { return out[c][i].date.Before(out[c][j].date) }) + } + return out, nil +} + +func resolveTryRate(currency string, target time.Time, series map[string][]ratePoint) float64 { + currency = strings.ToUpper(strings.TrimSpace(currency)) + if currency == "" || currency == "TRY" { + return 1 + } + points := series[currency] + if len(points) == 0 { + return 0 + } + + best := points[0] + bestDiff := absDurationDays(points[0].date.Sub(target)) + for i := 1; i < len(points); i++ { + diff := absDurationDays(points[i].date.Sub(target)) + if diff < bestDiff || (diff == bestDiff && points[i].date.After(best.date)) { + best = points[i] + bestDiff = diff + } + } + return best.rate +} + +func absDurationDays(d time.Duration) int64 { + if d < 0 { + d = -d + } + return int64(d.Hours() / 24) +} + +func parseDateOnly(v string) (time.Time, bool) { + v = strings.TrimSpace(v) + if v == "" { + return time.Time{}, false + } + t, err := time.Parse("2006-01-02", v) + if err != nil { + return time.Time{}, false + } + return t, true +} + +func toTRYByRate(amount float64, currency string, currTry float64) float64 { + currency = strings.ToUpper(strings.TrimSpace(currency)) + if currency == "" || currency == "TRY" { + return amount + } + if currTry <= 0 { + return 0 + } + return amount * currTry +} + +func toUSDByRates(amount float64, currency string, currTry, usdTry float64) float64 { + currency = strings.ToUpper(strings.TrimSpace(currency)) + switch currency { + case "USD": + return amount + case "", "TRY": + if usdTry <= 0 { + return 0 + } + return amount / usdTry + default: + if currTry <= 0 || usdTry <= 0 { + return 0 + } + return (amount * currTry) / usdTry + } +} + +// Returns X for "1 USD = X ". +func usdRateInCurrency(currency string, currTry, usdTry float64) float64 { + currency = strings.ToUpper(strings.TrimSpace(currency)) + switch currency { + case "", "USD": + return 1 + case "TRY": + if usdTry <= 0 { + return 0 + } + return usdTry + default: + if currTry <= 0 || usdTry <= 0 { + return 0 + } + return usdTry / currTry + } +} diff --git a/svc/routes/customer_balance_excel.go b/svc/routes/customer_balance_excel.go new file mode 100644 index 0000000..2a3b5e6 --- /dev/null +++ b/svc/routes/customer_balance_excel.go @@ -0,0 +1,145 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/models" + "bssapp-backend/queries" + "database/sql" + "fmt" + "net/http" + "strings" + "time" + + "github.com/xuri/excelize/v2" +) + +func ExportCustomerBalanceExcelHandler(_ *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + selectedDate := strings.TrimSpace(r.URL.Query().Get("selected_date")) + if selectedDate == "" { + selectedDate = time.Now().Format("2006-01-02") + } + + params := models.CustomerBalanceListParams{ + SelectedDate: selectedDate, + CariSearch: strings.TrimSpace(r.URL.Query().Get("cari_search")), + CariIlkGrup: strings.TrimSpace(r.URL.Query().Get("cari_ilk_grup")), + Piyasa: strings.TrimSpace(r.URL.Query().Get("piyasa")), + Temsilci: strings.TrimSpace(r.URL.Query().Get("temsilci")), + RiskDurumu: strings.TrimSpace(r.URL.Query().Get("risk_durumu")), + IslemTipi: strings.TrimSpace(r.URL.Query().Get("islem_tipi")), + Ulke: strings.TrimSpace(r.URL.Query().Get("ulke")), + Il: strings.TrimSpace(r.URL.Query().Get("il")), + Ilce: strings.TrimSpace(r.URL.Query().Get("ilce")), + } + + excludeZero12 := parseBoolQuery(r.URL.Query().Get("exclude_zero_12")) + excludeZero13 := parseBoolQuery(r.URL.Query().Get("exclude_zero_13")) + + rows, err := queries.GetCustomerBalanceList(r.Context(), params) + if err != nil { + http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError) + return + } + + rows = filterCustomerBalanceRowsForPDF(rows, excludeZero12, excludeZero13) + summaries, _ := buildCustomerBalancePDFData(rows) + + f := excelize.NewFile() + sheet := "CariBakiye" + f.SetSheetName("Sheet1", sheet) + + headers := []string{ + "Ana Cari Kodu", + "Ana Cari Detay", + "Piyasa", + "Temsilci", + "Risk Durumu", + "1_2 Bakiye Pr.Br", + "1_3 Bakiye Pr.Br", + "1_2 USD Bakiye", + "1_2 TRY Bakiye", + "1_3 USD Bakiye", + "1_3 TRY Bakiye", + } + + for i, h := range headers { + cell, _ := excelize.CoordinatesToCellName(i+1, 1) + f.SetCellValue(sheet, cell, h) + } + + var totalUSD12, totalTRY12, totalUSD13, totalTRY13 float64 + totalPrBr12 := map[string]float64{} + totalPrBr13 := map[string]float64{} + + for _, s := range summaries { + totalUSD12 += s.USDBakiye12 + totalTRY12 += s.TLBakiye12 + totalUSD13 += s.USDBakiye13 + totalTRY13 += s.TLBakiye13 + for k, v := range s.Bakiye12Map { + totalPrBr12[k] += v + } + for k, v := range s.Bakiye13Map { + totalPrBr13[k] += v + } + } + + f.SetSheetRow(sheet, "A2", &[]any{ + "TOPLAM", + "", + "", + "", + "", + formatCurrencyMapPDF(totalPrBr12), + formatCurrencyMapPDF(totalPrBr13), + totalUSD12, + totalTRY12, + totalUSD13, + totalTRY13, + }) + + rowNo := 3 + for _, s := range summaries { + f.SetSheetRow(sheet, fmt.Sprintf("A%d", rowNo), &[]any{ + s.AnaCariKodu, + s.AnaCariAdi, + s.Piyasa, + s.Temsilci, + s.RiskDurumu, + formatCurrencyMapPDF(s.Bakiye12Map), + formatCurrencyMapPDF(s.Bakiye13Map), + s.USDBakiye12, + s.TLBakiye12, + s.USDBakiye13, + s.TLBakiye13, + }) + rowNo++ + } + + _ = f.SetColWidth(sheet, "A", "A", 16) + _ = f.SetColWidth(sheet, "B", "B", 34) + _ = f.SetColWidth(sheet, "C", "E", 18) + _ = f.SetColWidth(sheet, "F", "G", 34) + _ = f.SetColWidth(sheet, "H", "K", 18) + + buf, err := f.WriteToBuffer() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + filename := fmt.Sprintf("cari_bakiye_listesi_%s.xlsx", time.Now().Format("20060102_150405")) + w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"") + w.Header().Set("Content-Length", fmt.Sprint(len(buf.Bytes()))) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(buf.Bytes()) + } +} diff --git a/svc/routes/customer_balance_list.go b/svc/routes/customer_balance_list.go index 42db345..b1129cf 100644 --- a/svc/routes/customer_balance_list.go +++ b/svc/routes/customer_balance_list.go @@ -33,6 +33,8 @@ func GetCustomerBalanceListHandler(w http.ResponseWriter, r *http.Request) { RiskDurumu: strings.TrimSpace(r.URL.Query().Get("risk_durumu")), IslemTipi: strings.TrimSpace(r.URL.Query().Get("islem_tipi")), Ulke: strings.TrimSpace(r.URL.Query().Get("ulke")), + Il: strings.TrimSpace(r.URL.Query().Get("il")), + Ilce: strings.TrimSpace(r.URL.Query().Get("ilce")), } rows, err := queries.GetCustomerBalanceList(r.Context(), params) diff --git a/svc/routes/customer_balance_pdf.go b/svc/routes/customer_balance_pdf.go new file mode 100644 index 0000000..27e1319 --- /dev/null +++ b/svc/routes/customer_balance_pdf.go @@ -0,0 +1,461 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/models" + "bssapp-backend/queries" + "bytes" + "database/sql" + "fmt" + "net/http" + "sort" + "strconv" + "strings" + "time" + + "github.com/jung-kurt/gofpdf" +) + +type balanceSummaryPDF struct { + AnaCariKodu string + AnaCariAdi string + Piyasa string + Temsilci string + RiskDurumu string + + Bakiye12Map map[string]float64 + Bakiye13Map map[string]float64 + USDBakiye12 float64 + TLBakiye12 float64 + USDBakiye13 float64 + TLBakiye13 float64 +} + +func ExportCustomerBalancePDFHandler(_ *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + selectedDate := strings.TrimSpace(r.URL.Query().Get("selected_date")) + if selectedDate == "" { + selectedDate = time.Now().Format("2006-01-02") + } + + params := models.CustomerBalanceListParams{ + SelectedDate: selectedDate, + CariSearch: strings.TrimSpace(r.URL.Query().Get("cari_search")), + CariIlkGrup: strings.TrimSpace(r.URL.Query().Get("cari_ilk_grup")), + Piyasa: strings.TrimSpace(r.URL.Query().Get("piyasa")), + Temsilci: strings.TrimSpace(r.URL.Query().Get("temsilci")), + RiskDurumu: strings.TrimSpace(r.URL.Query().Get("risk_durumu")), + IslemTipi: strings.TrimSpace(r.URL.Query().Get("islem_tipi")), + Ulke: strings.TrimSpace(r.URL.Query().Get("ulke")), + Il: strings.TrimSpace(r.URL.Query().Get("il")), + Ilce: strings.TrimSpace(r.URL.Query().Get("ilce")), + } + + detailed := parseBoolQuery(r.URL.Query().Get("detailed")) + excludeZero12 := parseBoolQuery(r.URL.Query().Get("exclude_zero_12")) + excludeZero13 := parseBoolQuery(r.URL.Query().Get("exclude_zero_13")) + + rows, err := queries.GetCustomerBalanceList(r.Context(), params) + if err != nil { + http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError) + return + } + + rows = filterCustomerBalanceRowsForPDF(rows, excludeZero12, excludeZero13) + summaries, detailsByMaster := buildCustomerBalancePDFData(rows) + + pdf := gofpdf.New("L", "mm", "A4", "") + pdf.SetMargins(8, 8, 8) + pdf.SetAutoPageBreak(false, 12) + if err := registerDejavuFonts(pdf, "dejavu"); err != nil { + http.Error(w, "pdf font error: "+err.Error(), http.StatusInternalServerError) + return + } + + drawCustomerBalancePDF( + pdf, + selectedDate, + params.CariSearch, + detailed, + summaries, + detailsByMaster, + ) + + if err := pdf.Error(); err != nil { + http.Error(w, "pdf render error: "+err.Error(), http.StatusInternalServerError) + return + } + + var buf bytes.Buffer + if err := pdf.Output(&buf); err != nil { + http.Error(w, "pdf output error: "+err.Error(), http.StatusInternalServerError) + return + } + + filename := "customer-balance-summary.pdf" + if detailed { + filename = "customer-balance-detailed.pdf" + } + + w.Header().Set("Content-Type", "application/pdf") + w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", filename)) + _, _ = w.Write(buf.Bytes()) + } +} + +func parseBoolQuery(v string) bool { + switch strings.ToLower(strings.TrimSpace(v)) { + case "1", "true", "yes", "on": + return true + default: + return false + } +} + +func filterCustomerBalanceRowsForPDF(rows []models.CustomerBalanceListRow, excludeZero12, excludeZero13 bool) []models.CustomerBalanceListRow { + out := make([]models.CustomerBalanceListRow, 0, len(rows)) + for _, row := range rows { + if excludeZero12 && row.Bakiye12 == 0 { + continue + } + if excludeZero13 && row.Bakiye13 == 0 { + continue + } + out = append(out, row) + } + return out +} + +func buildCustomerBalancePDFData(rows []models.CustomerBalanceListRow) ([]balanceSummaryPDF, map[string][]models.CustomerBalanceListRow) { + summaryMap := make(map[string]*balanceSummaryPDF) + detailsByMaster := make(map[string][]models.CustomerBalanceListRow) + + for _, row := range rows { + master := strings.TrimSpace(row.AnaCariKodu) + if master == "" { + master = strings.TrimSpace(row.CariKodu) + } + if master == "" { + continue + } + + s := summaryMap[master] + if s == nil { + s = &balanceSummaryPDF{ + AnaCariKodu: master, + AnaCariAdi: strings.TrimSpace(row.AnaCariAdi), + Piyasa: strings.TrimSpace(row.Piyasa), + Temsilci: strings.TrimSpace(row.Temsilci), + RiskDurumu: strings.TrimSpace(row.RiskDurumu), + Bakiye12Map: map[string]float64{}, + Bakiye13Map: map[string]float64{}, + } + summaryMap[master] = s + } + + if s.AnaCariAdi == "" && strings.TrimSpace(row.AnaCariAdi) != "" { + s.AnaCariAdi = strings.TrimSpace(row.AnaCariAdi) + } + if s.Piyasa == "" && strings.TrimSpace(row.Piyasa) != "" { + s.Piyasa = strings.TrimSpace(row.Piyasa) + } + if s.Temsilci == "" && strings.TrimSpace(row.Temsilci) != "" { + s.Temsilci = strings.TrimSpace(row.Temsilci) + } + if s.RiskDurumu == "" && strings.TrimSpace(row.RiskDurumu) != "" { + s.RiskDurumu = strings.TrimSpace(row.RiskDurumu) + } + + curr := strings.ToUpper(strings.TrimSpace(row.CariDoviz)) + if curr == "" { + curr = "N/A" + } + s.Bakiye12Map[curr] += row.Bakiye12 + s.Bakiye13Map[curr] += row.Bakiye13 + s.USDBakiye12 += row.USDBakiye12 + s.TLBakiye12 += row.TLBakiye12 + s.USDBakiye13 += row.USDBakiye13 + s.TLBakiye13 += row.TLBakiye13 + + detailsByMaster[master] = append(detailsByMaster[master], row) + } + + masters := make([]string, 0, len(summaryMap)) + for m := range summaryMap { + masters = append(masters, m) + } + sort.Strings(masters) + + summaries := make([]balanceSummaryPDF, 0, len(masters)) + for _, m := range masters { + summaries = append(summaries, *summaryMap[m]) + d := detailsByMaster[m] + sort.SliceStable(d, func(i, j int) bool { + if d[i].CariKodu == d[j].CariKodu { + if d[i].CariDoviz == d[j].CariDoviz { + si, _ := strconv.Atoi(d[i].Sirket) + sj, _ := strconv.Atoi(d[j].Sirket) + return si < sj + } + return d[i].CariDoviz < d[j].CariDoviz + } + return d[i].CariKodu < d[j].CariKodu + }) + detailsByMaster[m] = d + } + + return summaries, detailsByMaster +} + +func drawCustomerBalancePDF( + pdf *gofpdf.Fpdf, + selectedDate string, + searchText string, + detailed bool, + summaries []balanceSummaryPDF, + detailsByMaster map[string][]models.CustomerBalanceListRow, +) { + pageW, _ := pdf.GetPageSize() + marginL, marginT, marginR, marginB := 8.0, 8.0, 8.0, 12.0 + tableW := pageW - marginL - marginR + + summaryCols := []string{"Ana Cari Kod", "Ana Cari Detay", "Piyasa", "Temsilci", "Risk", "1_2 Pr.Br", "1_3 Pr.Br", "1_2 USD", "1_2 TRY", "1_3 USD", "1_3 TRY"} + summaryW := normalizeWidths([]float64{20, 43, 18, 18, 16, 27, 27, 15, 15, 15, 15}, tableW) + + detailCols := []string{"Cari Kod", "Cari Detay", "Sirket", "Muhasebe", "Doviz", "1_2 Pr.Br", "1_3 Pr.Br", "1_2 USD", "1_2 TRY", "1_3 USD", "1_3 TRY"} + detailW := normalizeWidths([]float64{26, 46, 10, 20, 10, 24, 24, 15, 15, 15, 15}, tableW) + + header := func() { + pdf.AddPage() + pdf.SetFont("dejavu", "B", 15) + pdf.SetTextColor(149, 113, 22) + pdf.SetXY(marginL, marginT) + pdf.CellFormat(120, 7, "Cari Bakiye Listesi", "", 0, "L", false, 0, "") + + pdf.SetFont("dejavu", "", 9) + pdf.SetTextColor(20, 20, 20) + pdf.SetXY(pageW-marginR-80, marginT+1) + pdf.CellFormat(80, 5, "Tarih: "+selectedDate, "", 0, "R", false, 0, "") + + mode := "Detaysiz" + if detailed { + mode = "Detayli" + } + pdf.SetXY(pageW-marginR-80, marginT+6) + pdf.CellFormat(80, 5, "Mod: "+mode, "", 0, "R", false, 0, "") + + if strings.TrimSpace(searchText) != "" { + pdf.SetXY(marginL, marginT+8) + pdf.CellFormat(tableW, 5, "Arama: "+searchText, "", 0, "L", false, 0, "") + } + + pdf.SetDrawColor(149, 113, 22) + pdf.Line(marginL, marginT+14, pageW-marginR, marginT+14) + pdf.SetDrawColor(210, 210, 210) + pdf.SetY(marginT + 17) + } + + needPage := func(needH float64) bool { + return pdf.GetY()+needH+marginB > 210.0 + } + + drawSummaryHeader := func() { + pdf.SetFont("dejavu", "B", 7.5) + pdf.SetFillColor(149, 113, 22) + pdf.SetTextColor(255, 255, 255) + y := pdf.GetY() + x := marginL + for i, c := range summaryCols { + pdf.Rect(x, y, summaryW[i], 7, "DF") + pdf.SetXY(x+1, y+1.2) + pdf.CellFormat(summaryW[i]-2, 4.6, c, "", 0, "C", false, 0, "") + x += summaryW[i] + } + pdf.SetY(y + 7) + } + + drawDetailHeader := func() { + pdf.SetFont("dejavu", "B", 7.2) + pdf.SetFillColor(149, 113, 22) + pdf.SetTextColor(255, 255, 255) + y := pdf.GetY() + x := marginL + for i, c := range detailCols { + pdf.Rect(x, y, detailW[i], 6, "DF") + pdf.SetXY(x+1, y+1) + pdf.CellFormat(detailW[i]-2, 4, c, "", 0, "C", false, 0, "") + x += detailW[i] + } + pdf.SetY(y + 6) + } + + header() + drawSummaryHeader() + + pdf.SetFont("dejavu", "", 7.2) + pdf.SetTextColor(20, 20, 20) + + for _, s := range summaries { + if needPage(6.2) { + header() + drawSummaryHeader() + } + + row := []string{ + s.AnaCariKodu, + s.AnaCariAdi, + s.Piyasa, + s.Temsilci, + s.RiskDurumu, + formatCurrencyMapPDF(s.Bakiye12Map), + formatCurrencyMapPDF(s.Bakiye13Map), + formatMoneyPDF(s.USDBakiye12), + formatMoneyPDF(s.TLBakiye12), + formatMoneyPDF(s.USDBakiye13), + formatMoneyPDF(s.TLBakiye13), + } + + y := pdf.GetY() + x := marginL + for i, v := range row { + pdf.Rect(x, y, summaryW[i], 6.2, "") + align := "L" + if i >= 7 { + align = "R" + } + pdf.SetXY(x+1, y+1) + pdf.CellFormat(summaryW[i]-2, 4.2, v, "", 0, align, false, 0, "") + x += summaryW[i] + } + pdf.SetY(y + 6.2) + } + + if !detailed { + return + } + + pdf.Ln(1.8) + for _, s := range summaries { + rows := detailsByMaster[s.AnaCariKodu] + if len(rows) == 0 { + continue + } + + if needPage(12.4) { + header() + } + + pdf.SetFont("dejavu", "B", 8) + pdf.SetFillColor(218, 193, 151) + pdf.SetTextColor(20, 20, 20) + y := pdf.GetY() + pdf.Rect(marginL, y, tableW, 6.2, "DF") + pdf.SetXY(marginL+1.5, y+1) + pdf.CellFormat(tableW-3, 4.2, "Detay: "+s.AnaCariKodu, "", 0, "L", false, 0, "") + pdf.SetY(y + 6.2) + drawDetailHeader() + + pdf.SetFont("dejavu", "", 7) + pdf.SetTextColor(40, 40, 40) + + for _, r := range rows { + if needPage(5.8) { + header() + pdf.SetFont("dejavu", "B", 8) + pdf.SetFillColor(218, 193, 151) + pdf.SetTextColor(20, 20, 20) + y := pdf.GetY() + pdf.Rect(marginL, y, tableW, 6.2, "DF") + pdf.SetXY(marginL+1.5, y+1) + pdf.CellFormat(tableW-3, 4.2, "Detay: "+s.AnaCariKodu, "", 0, "L", false, 0, "") + pdf.SetY(y + 6.2) + drawDetailHeader() + pdf.SetFont("dejavu", "", 7) + pdf.SetTextColor(40, 40, 40) + } + + line := []string{ + r.CariKodu, + r.CariDetay, + r.Sirket, + r.MuhasebeKodu, + r.CariDoviz, + formatMoneyPDF(r.Bakiye12), + formatMoneyPDF(r.Bakiye13), + formatMoneyPDF(r.USDBakiye12), + formatMoneyPDF(r.TLBakiye12), + formatMoneyPDF(r.USDBakiye13), + formatMoneyPDF(r.TLBakiye13), + } + + rowY := pdf.GetY() + rowX := marginL + for i, v := range line { + pdf.Rect(rowX, rowY, detailW[i], 5.8, "") + align := "L" + if i >= 5 { + align = "R" + } + pdf.SetXY(rowX+1, rowY+0.8) + pdf.CellFormat(detailW[i]-2, 4.0, v, "", 0, align, false, 0, "") + rowX += detailW[i] + } + pdf.SetY(rowY + 5.8) + } + pdf.Ln(1.2) + } +} + +func formatCurrencyMapPDF(m map[string]float64) string { + if len(m) == 0 { + return "-" + } + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + parts := make([]string, 0, len(keys)) + for _, k := range keys { + if m[k] == 0 { + continue + } + parts = append(parts, k+": "+formatMoneyPDF(m[k])) + } + if len(parts) == 0 { + return "-" + } + return strings.Join(parts, " | ") +} + +func formatMoneyPDF(v float64) string { + s := fmt.Sprintf("%.2f", v) + parts := strings.SplitN(s, ".", 2) + intPart, decPart := parts[0], "00" + if len(parts) == 2 { + decPart = parts[1] + } + + sign := "" + if strings.HasPrefix(intPart, "-") { + sign = "-" + intPart = strings.TrimPrefix(intPart, "-") + } + + var out []string + for len(intPart) > 3 { + out = append([]string{intPart[len(intPart)-3:]}, out...) + intPart = intPart[:len(intPart)-3] + } + if intPart != "" { + out = append([]string{intPart}, out...) + } + + return sign + strings.Join(out, ".") + "," + decPart +} diff --git a/svc/routes/statement_aging.go b/svc/routes/statement_aging.go new file mode 100644 index 0000000..0d753d9 --- /dev/null +++ b/svc/routes/statement_aging.go @@ -0,0 +1,41 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/models" + "bssapp-backend/queries" + "encoding/json" + "net/http" + "strings" +) + +// GET /api/finance/account-aging-statement +func GetStatementAgingHandler(w http.ResponseWriter, r *http.Request) { + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + params := models.StatementAgingParams{ + AccountCode: strings.TrimSpace(r.URL.Query().Get("accountcode")), + EndDate: strings.TrimSpace(r.URL.Query().Get("enddate")), + Parislemler: r.URL.Query()["parislemler"], + } + + if params.AccountCode == "" || params.EndDate == "" { + http.Error(w, "accountcode and enddate are required", http.StatusBadRequest) + return + } + + rows, err := queries.GetStatementAging(params) + if err != nil { + http.Error(w, "Error fetching aging statement: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if err := json.NewEncoder(w).Encode(rows); err != nil { + http.Error(w, "Error encoding response: "+err.Error(), http.StatusInternalServerError) + } +} diff --git a/svc/routes/statement_aging_pdf.go b/svc/routes/statement_aging_pdf.go new file mode 100644 index 0000000..217139d --- /dev/null +++ b/svc/routes/statement_aging_pdf.go @@ -0,0 +1,573 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/models" + "bssapp-backend/queries" + "bytes" + "database/sql" + "fmt" + "math" + "net/http" + "sort" + "strconv" + "strings" + "time" + + "github.com/jung-kurt/gofpdf" +) + +type agingDetailPDF struct { + FaturaCari string + OdemeCari string + Doviz string + FaturaRef string + OdemeRef string + FaturaTarihi string + OdemeTarihi string + OdemeDocDate string + EslesenTutar float64 + UsdTutar float64 + TryTutar float64 + Aciklama string + Gun int + GunBelge int + GunKur float64 + odemeDateParsed time.Time + odemeDateEmpty bool +} + +type agingCurrencyPDF struct { + Key string + Cari8 string + CariDetay string + Doviz string + AcikKalemTutar float64 + AcikKalemUSD float64 + AcikKalemTRY float64 + OrtGun int + OrtBelgeGun int + weightedBase float64 + weightedGunSum float64 + weightedDocSum float64 + Details []agingDetailPDF +} + +type agingMasterPDF struct { + Key string + Cari8 string + CariDetay string + AcikKalemUSD float64 + AcikKalemTRY float64 + AcikKalemOrtVadeGun int + AcikKalemOrtBelge int + NormalUSD float64 + NormalTRY float64 + OrtalamaVadeGun int + OrtalamaBelgeGun int + weightedAllBase float64 + weightedAllGunSum float64 + weightedAllDocSum float64 + weightedOpenBase float64 + weightedOpenGunSum float64 + weightedOpenDocSum float64 + Currencies []agingCurrencyPDF +} + +func ExportStatementAgingPDFHandler(_ *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + params := models.StatementAgingParams{ + AccountCode: strings.TrimSpace(r.URL.Query().Get("accountcode")), + EndDate: strings.TrimSpace(r.URL.Query().Get("enddate")), + Parislemler: r.URL.Query()["parislemler"], + } + if params.AccountCode == "" || params.EndDate == "" { + http.Error(w, "accountcode and enddate are required", http.StatusBadRequest) + return + } + + rows, err := queries.GetStatementAging(params) + if err != nil { + http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError) + return + } + + masters := buildAgingPDFData(rows) + + pdf := gofpdf.New("L", "mm", "A4", "") + pdf.SetMargins(8, 8, 8) + pdf.SetAutoPageBreak(false, 10) + if err := registerDejavuFonts(pdf, "dejavu"); err != nil { + http.Error(w, "pdf font error: "+err.Error(), http.StatusInternalServerError) + return + } + + drawStatementAgingPDF(pdf, params, masters) + + if err := pdf.Error(); err != nil { + http.Error(w, "pdf render error: "+err.Error(), http.StatusInternalServerError) + return + } + + var buf bytes.Buffer + if err := pdf.Output(&buf); err != nil { + http.Error(w, "pdf output error: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/pdf") + w.Header().Set("Content-Disposition", `inline; filename="account-aging-detailed.pdf"`) + _, _ = w.Write(buf.Bytes()) + } +} + +func buildAgingPDFData(rows []map[string]interface{}) []agingMasterPDF { + masterMap := make(map[string]*agingMasterPDF) + currMap := make(map[string]*agingCurrencyPDF) + + for _, row := range rows { + cari8 := strings.TrimSpace(readStrAny(row["Cari8"], row["cari8"])) + if cari8 == "" { + continue + } + cariDetay := strings.TrimSpace(readStrAny(row["CariDetay"], row["cari_detay"])) + doviz := strings.ToUpper(strings.TrimSpace(readStrAny(row["DocCurrencyCode"], row["doc_currency_code"]))) + if doviz == "" { + doviz = "TRY" + } + aciklama := strings.ToUpper(strings.TrimSpace(readStrAny(row["Aciklama"], row["aciklama"]))) + isAcik := aciklama == "ACIKKALEM" + + eslesen := readFloatAny(row["EslesenTutar"], row["eslesen_tutar"]) + usd := readFloatAny(row["UsdTutar"], row["usd_tutar"]) + tryVal := readFloatAny(row["TryTutar"], row["try_tutar"]) + gun := readIntAny(row["GunSayisi"], row["gun_sayisi"]) + gunBelge := readIntAny(row["GunSayisi_DocDate"], row["gun_sayisi_docdate"]) + absTry := math.Abs(tryVal) + + m := masterMap[cari8] + if m == nil { + m = &agingMasterPDF{Key: cari8, Cari8: cari8, CariDetay: cariDetay} + masterMap[cari8] = m + } + if m.CariDetay == "" && cariDetay != "" { + m.CariDetay = cariDetay + } + + ckey := cari8 + "|" + doviz + c := currMap[ckey] + if c == nil { + c = &agingCurrencyPDF{ + Key: ckey, + Cari8: cari8, + CariDetay: cariDetay, + Doviz: doviz, + Details: make([]agingDetailPDF, 0, 128), + } + currMap[ckey] = c + } + if c.CariDetay == "" && cariDetay != "" { + c.CariDetay = cariDetay + } + + if isAcik { + m.AcikKalemUSD += usd + m.AcikKalemTRY += tryVal + c.AcikKalemTutar += eslesen + c.AcikKalemUSD += usd + c.AcikKalemTRY += tryVal + } else { + m.NormalUSD += usd + m.NormalTRY += tryVal + } + + if absTry > 0 { + m.weightedAllBase += absTry + m.weightedAllGunSum += absTry * float64(gun) + m.weightedAllDocSum += absTry * float64(gunBelge) + + if isAcik { + m.weightedOpenBase += absTry + m.weightedOpenGunSum += absTry * float64(gun) + m.weightedOpenDocSum += absTry * float64(gunBelge) + + c.weightedBase += absTry + c.weightedGunSum += absTry * float64(gun) + c.weightedDocSum += absTry * float64(gunBelge) + } + } + + odemeTar := readStrAny(row["OdemeTarihi"], row["odeme_tarihi"]) + odemeParsed, ok := parseYMD(odemeTar) + detail := agingDetailPDF{ + FaturaCari: readStrAny(row["FaturaCari"], row["fatura_cari"]), + OdemeCari: readStrAny(row["OdemeCari"], row["odeme_cari"]), + Doviz: doviz, + FaturaRef: readStrAny(row["FaturaRef"], row["fatura_ref"]), + OdemeRef: readStrAny(row["OdemeRef"], row["odeme_ref"]), + FaturaTarihi: readStrAny(row["FaturaTarihi"], row["fatura_tarihi"]), + OdemeTarihi: odemeTar, + OdemeDocDate: readStrAny(row["OdemeDocDate"], row["odeme_doc_date"]), + EslesenTutar: eslesen, + UsdTutar: usd, + TryTutar: tryVal, + Aciklama: readStrAny(row["Aciklama"], row["aciklama"]), + Gun: gun, + GunBelge: gunBelge, + GunKur: readFloatAny(row["GunKur"], row["gun_kur"]), + odemeDateParsed: odemeParsed, + odemeDateEmpty: !ok, + } + c.Details = append(c.Details, detail) + } + + masters := make([]agingMasterPDF, 0, len(masterMap)) + for _, m := range masterMap { + if m.weightedOpenBase > 0 { + m.AcikKalemOrtVadeGun = int(math.Ceil(m.weightedOpenGunSum / m.weightedOpenBase)) + m.AcikKalemOrtBelge = int(math.Ceil(m.weightedOpenDocSum / m.weightedOpenBase)) + } + if m.weightedAllBase > 0 { + m.OrtalamaVadeGun = int(math.Ceil(m.weightedAllGunSum / m.weightedAllBase)) + m.OrtalamaBelgeGun = int(math.Ceil(m.weightedAllDocSum / m.weightedAllBase)) + } + + currs := make([]agingCurrencyPDF, 0, len(currMap)) + for _, c := range currMap { + if c.Cari8 != m.Cari8 { + continue + } + if c.weightedBase > 0 { + c.OrtGun = int(math.Ceil(c.weightedGunSum / c.weightedBase)) + c.OrtBelgeGun = int(math.Ceil(c.weightedDocSum / c.weightedBase)) + } + sort.SliceStable(c.Details, func(i, j int) bool { + ai := c.Details[i] + aj := c.Details[j] + if ai.odemeDateEmpty && !aj.odemeDateEmpty { + return true + } + if !ai.odemeDateEmpty && aj.odemeDateEmpty { + return false + } + if ai.odemeDateEmpty && aj.odemeDateEmpty { + return false + } + return ai.odemeDateParsed.After(aj.odemeDateParsed) + }) + currs = append(currs, *c) + } + sort.SliceStable(currs, func(i, j int) bool { return currs[i].Doviz < currs[j].Doviz }) + m.Currencies = currs + masters = append(masters, *m) + } + + sort.SliceStable(masters, func(i, j int) bool { return masters[i].Cari8 < masters[j].Cari8 }) + return masters +} + +func drawStatementAgingPDF(pdf *gofpdf.Fpdf, p models.StatementAgingParams, masters []agingMasterPDF) { + pageW, pageH := pdf.GetPageSize() + marginL, marginR, marginT, marginB := 8.0, 8.0, 8.0, 10.0 + tableW := pageW - marginL - marginR + + colorPrimary := [3]int{149, 113, 22} + colorLevel2 := [3]int{76, 95, 122} + colorLevel3 := [3]int{31, 59, 91} + + level1Cols := []string{"Ana Cari Kod", "Ana Cari Detay", "Açık Kalem USD", "Açık Kalem TRY", "Açık Kalem Ort Vade", "Açık Kalem Ort Belge", "Normal USD", "Normal TRY", "Ortalama Vade", "Ortalama Belge"} + level1W := normalizeWidths([]float64{20, 46, 18, 18, 15, 15, 16, 16, 15, 15}, tableW) + + level2Cols := []string{"Ana Cari Kod", "Ana Cari Detay", "Döviz", "Açık Kalem", "Açık Kalem USD", "Açık Kalem TRY", "Ort Gün", "Ort Belge Gün"} + level2W := normalizeWidths([]float64{20, 52, 12, 24, 24, 24, 16, 18}, tableW) + + level3Cols := []string{"Fatura Cari", "Ödeme Cari", "Döviz", "Fatura Ref", "Ödeme Ref", "Fatura Tarihi", "Ödeme Vade", "Ödeme DocDate", "Eşleşen", "USD", "TRY", "Açıklama", "Gün", "Gün Belge", "Gün Kur"} + level3W := normalizeWidths([]float64{15, 15, 10, 15, 15, 13, 13, 13, 14, 12, 14, 14, 8, 10, 10}, tableW) + + pageHeader := func() { + pdf.AddPage() + pdf.SetFont("dejavu", "B", 15) + pdf.SetTextColor(colorPrimary[0], colorPrimary[1], colorPrimary[2]) + pdf.SetXY(marginL, marginT) + pdf.CellFormat(150, 7, "Cari Yaşlandırmalı Ekstre", "", 0, "L", false, 0, "") + + pdf.SetFont("dejavu", "", 9) + pdf.SetTextColor(20, 20, 20) + pdf.SetXY(pageW-marginR-95, marginT+1) + pdf.CellFormat(95, 5, "Son Tarih: "+p.EndDate, "", 0, "R", false, 0, "") + pdf.SetXY(pageW-marginR-95, marginT+6) + pdf.CellFormat(95, 5, "Cari: "+p.AccountCode, "", 0, "R", false, 0, "") + + mode := "1_2" + if len(p.Parislemler) > 0 { + mode = strings.Join(p.Parislemler, ",") + } + pdf.SetXY(pageW-marginR-95, marginT+11) + pdf.CellFormat(95, 5, "Parasal İşlem: "+mode, "", 0, "R", false, 0, "") + + pdf.SetDrawColor(colorPrimary[0], colorPrimary[1], colorPrimary[2]) + pdf.Line(marginL, marginT+16, pageW-marginR, marginT+16) + pdf.SetDrawColor(210, 210, 210) + pdf.SetY(marginT + 19) + } + + needPage := func(need float64) bool { + return pdf.GetY()+need+marginB > pageH + } + + drawHeaderRow := func(cols []string, widths []float64, rgb [3]int, h float64) { + pdf.SetFont("dejavu", "B", 8.4) + pdf.SetFillColor(rgb[0], rgb[1], rgb[2]) + pdf.SetTextColor(255, 255, 255) + y := pdf.GetY() + x := marginL + for i, c := range cols { + pdf.Rect(x, y, widths[i], h, "DF") + pdf.SetXY(x+0.8, y+1.0) + pdf.CellFormat(widths[i]-1.6, h-2.0, c, "", 0, "C", false, 0, "") + x += widths[i] + } + pdf.SetY(y + h) + } + + drawRow := func(vals []string, widths []float64, h float64, fill bool, fillRGB [3]int, centerCols map[int]bool, rightCols map[int]bool) { + pdf.SetFont("dejavu", "", 7.6) + pdf.SetTextColor(20, 20, 20) + y := pdf.GetY() + x := marginL + for i, v := range vals { + if fill { + pdf.SetFillColor(fillRGB[0], fillRGB[1], fillRGB[2]) + pdf.Rect(x, y, widths[i], h, "DF") + } else { + pdf.Rect(x, y, widths[i], h, "") + } + align := "L" + if rightCols[i] { + align = "R" + } else if centerCols[i] { + align = "C" + } + pdf.SetXY(x+0.8, y+0.8) + pdf.CellFormat(widths[i]-1.6, h-1.6, v, "", 0, align, false, 0, "") + x += widths[i] + } + pdf.SetY(y + h) + } + + format2 := func(v float64) string { + return trFormat(v, 2) + } + + pageHeader() + for _, m := range masters { + if needPage(7 + 7 + 6) { + pageHeader() + } + + drawHeaderRow(level1Cols, level1W, colorPrimary, 7) + drawRow( + []string{ + m.Cari8, m.CariDetay, + format2(m.AcikKalemUSD), format2(m.AcikKalemTRY), + fmt.Sprintf("%d", m.AcikKalemOrtVadeGun), fmt.Sprintf("%d", m.AcikKalemOrtBelge), + format2(m.NormalUSD), format2(m.NormalTRY), + fmt.Sprintf("%d", m.OrtalamaVadeGun), fmt.Sprintf("%d", m.OrtalamaBelgeGun), + }, + level1W, 6.4, true, [3]int{250, 246, 238}, + map[int]bool{0: true, 4: true, 5: true, 8: true, 9: true}, + map[int]bool{2: true, 3: true, 6: true, 7: true}, + ) + + for _, c := range m.Currencies { + if needPage(6 + 6 + 6) { + pageHeader() + drawHeaderRow(level1Cols, level1W, colorPrimary, 7) + drawRow( + []string{ + m.Cari8, m.CariDetay, + format2(m.AcikKalemUSD), format2(m.AcikKalemTRY), + fmt.Sprintf("%d", m.AcikKalemOrtVadeGun), fmt.Sprintf("%d", m.AcikKalemOrtBelge), + format2(m.NormalUSD), format2(m.NormalTRY), + fmt.Sprintf("%d", m.OrtalamaVadeGun), fmt.Sprintf("%d", m.OrtalamaBelgeGun), + }, + level1W, 6.4, true, [3]int{250, 246, 238}, + map[int]bool{0: true, 4: true, 5: true, 8: true, 9: true}, + map[int]bool{2: true, 3: true, 6: true, 7: true}, + ) + } + + drawHeaderRow(level2Cols, level2W, colorLevel2, 6) + drawRow( + []string{ + c.Cari8, c.CariDetay, c.Doviz, + format2(c.AcikKalemTutar), format2(c.AcikKalemUSD), format2(c.AcikKalemTRY), + fmt.Sprintf("%d", c.OrtGun), fmt.Sprintf("%d", c.OrtBelgeGun), + }, + level2W, 5.8, true, [3]int{236, 240, 247}, + map[int]bool{0: true, 2: true, 6: true, 7: true}, + map[int]bool{3: true, 4: true, 5: true}, + ) + + if needPage(5.8) { + pageHeader() + } + drawHeaderRow(level3Cols, level3W, colorLevel3, 5.8) + + for _, d := range c.Details { + if needPage(5.2) { + pageHeader() + drawHeaderRow(level3Cols, level3W, colorLevel3, 5.8) + } + drawRow( + []string{ + d.FaturaCari, d.OdemeCari, d.Doviz, d.FaturaRef, d.OdemeRef, + d.FaturaTarihi, d.OdemeTarihi, d.OdemeDocDate, + format2(d.EslesenTutar), format2(d.UsdTutar), format2(d.TryTutar), d.Aciklama, + fmt.Sprintf("%d", d.Gun), fmt.Sprintf("%d", d.GunBelge), trFormat(d.GunKur, 2), + }, + level3W, 5.2, false, [3]int{}, + map[int]bool{2: true, 5: true, 6: true, 7: true, 11: true, 12: true, 13: true, 14: true}, + map[int]bool{8: true, 9: true, 10: true}, + ) + } + pdf.Ln(1.2) + } + pdf.Ln(1.8) + } +} + +func trFormat(v float64, frac int) string { + neg := v < 0 + if neg { + v = -v + } + pow := math.Pow(10, float64(frac)) + rounded := math.Round(v*pow) / pow + intPart := int64(rounded) + decPart := int64(math.Round((rounded - float64(intPart)) * pow)) + + intStr := fmt.Sprintf("%d", intPart) + var grouped strings.Builder + for i, r := range intStr { + if i > 0 && (len(intStr)-i)%3 == 0 { + grouped.WriteString(".") + } + grouped.WriteRune(r) + } + + out := grouped.String() + if frac > 0 { + decFmt := fmt.Sprintf("%%0%dd", frac) + out += "," + fmt.Sprintf(decFmt, decPart) + } + if neg { + return "-" + out + } + return out +} + +func readStrAny(v ...interface{}) string { + for _, x := range v { + switch t := x.(type) { + case nil: + case string: + if strings.TrimSpace(t) != "" { + return t + } + case []byte: + s := strings.TrimSpace(string(t)) + if s != "" { + return s + } + case time.Time: + return t.Format("2006-01-02") + default: + s := strings.TrimSpace(fmt.Sprint(t)) + if s != "" && s != "" { + return s + } + } + } + return "" +} + +func readFloatAny(v ...interface{}) float64 { + for _, x := range v { + if x == nil { + continue + } + switch t := x.(type) { + case float64: + return t + case float32: + return float64(t) + case int: + return float64(t) + case int32: + return float64(t) + case int64: + return float64(t) + case string: + if n, ok := parseNumberFlexible(t); ok { + return n + } + case []byte: + if n, ok := parseNumberFlexible(string(t)); ok { + return n + } + default: + if n, ok := parseNumberFlexible(fmt.Sprint(t)); ok { + return n + } + } + } + return 0 +} + +func readIntAny(v ...interface{}) int { + return int(math.Ceil(readFloatAny(v...))) +} + +func parseNumberFlexible(s string) (float64, bool) { + s = strings.TrimSpace(s) + if s == "" { + return 0, false + } + hasComma := strings.Contains(s, ",") + hasDot := strings.Contains(s, ".") + if hasComma && hasDot { + if strings.LastIndex(s, ",") > strings.LastIndex(s, ".") { + s = strings.ReplaceAll(s, ".", "") + s = strings.Replace(s, ",", ".", 1) + } else { + s = strings.ReplaceAll(s, ",", "") + } + } else if hasComma { + s = strings.ReplaceAll(s, ".", "") + s = strings.Replace(s, ",", ".", 1) + } + n, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0, false + } + return n, true +} + +func parseYMD(v string) (time.Time, bool) { + v = strings.TrimSpace(v) + if v == "" { + return time.Time{}, false + } + t, err := time.Parse("2006-01-02", v) + if err != nil { + return time.Time{}, false + } + return t, true +} diff --git a/ui/.quasar/prod-spa/app.js b/ui/.quasar/prod-spa/app.js new file mode 100644 index 0000000..caeaac1 --- /dev/null +++ b/ui/.quasar/prod-spa/app.js @@ -0,0 +1,75 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * You are probably looking on adding startup/initialization code. + * Use "quasar new boot " and add it there. + * One boot file per concern. Then reference the file(s) in quasar.config file > boot: + * boot: ['file', ...] // do not add ".js" extension to it. + * + * Boot files are your "main.js" + **/ + + + + + +import { Quasar } from 'quasar' +import { markRaw } from 'vue' +import RootComponent from 'app/src/App.vue' + +import createStore from 'app/src/stores/index' +import createRouter from 'app/src/router/index' + + + + + +export default async function (createAppFn, quasarUserOptions) { + + + // Create the app instance. + // Here we inject into it the Quasar UI, the router & possibly the store. + const app = createAppFn(RootComponent) + + + + app.use(Quasar, quasarUserOptions) + + + + + const store = typeof createStore === 'function' + ? await createStore({}) + : createStore + + + app.use(store) + + + + + + const router = markRaw( + typeof createRouter === 'function' + ? await createRouter({store}) + : createRouter + ) + + + // make router instance available in store + + store.use(({ store }) => { store.router = router }) + + + + // Expose the app, the router and the store. + // Note that we are not mounting the app here, since bootstrapping will be + // different depending on whether we are in a browser or on the server. + return { + app, + store, + router + } +} diff --git a/ui/.quasar/prod-spa/client-entry.js b/ui/.quasar/prod-spa/client-entry.js new file mode 100644 index 0000000..5de66d0 --- /dev/null +++ b/ui/.quasar/prod-spa/client-entry.js @@ -0,0 +1,154 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * You are probably looking on adding startup/initialization code. + * Use "quasar new boot " and add it there. + * One boot file per concern. Then reference the file(s) in quasar.config file > boot: + * boot: ['file', ...] // do not add ".js" extension to it. + * + * Boot files are your "main.js" + **/ + + +import { createApp } from 'vue' + + + + + + + +import '@quasar/extras/roboto-font/roboto-font.css' + +import '@quasar/extras/material-icons/material-icons.css' + + + + +// We load Quasar stylesheet file +import 'quasar/dist/quasar.sass' + + + + +import 'src/css/app.css' + + +import createQuasarApp from './app.js' +import quasarUserOptions from './quasar-user-options.js' + + + + + + + + +const publicPath = `/` + + +async function start ({ + app, + router + , store +}, bootFiles) { + + let hasRedirected = false + const getRedirectUrl = url => { + try { return router.resolve(url).href } + catch (err) {} + + return Object(url) === url + ? null + : url + } + const redirect = url => { + hasRedirected = true + + if (typeof url === 'string' && /^https?:\/\//.test(url)) { + window.location.href = url + return + } + + const href = getRedirectUrl(url) + + // continue if we didn't fail to resolve the url + if (href !== null) { + window.location.href = href + window.location.reload() + } + } + + const urlPath = window.location.href.replace(window.location.origin, '') + + for (let i = 0; hasRedirected === false && i < bootFiles.length; i++) { + try { + await bootFiles[i]({ + app, + router, + store, + ssrContext: null, + redirect, + urlPath, + publicPath + }) + } + catch (err) { + if (err && err.url) { + redirect(err.url) + return + } + + console.error('[Quasar] boot error:', err) + return + } + } + + if (hasRedirected === true) return + + + app.use(router) + + + + + + + app.mount('#q-app') + + + +} + +createQuasarApp(createApp, quasarUserOptions) + + .then(app => { + // eventually remove this when Cordova/Capacitor/Electron support becomes old + const [ method, mapFn ] = Promise.allSettled !== void 0 + ? [ + 'allSettled', + bootFiles => bootFiles.map(result => { + if (result.status === 'rejected') { + console.error('[Quasar] boot error:', result.reason) + return + } + return result.value.default + }) + ] + : [ + 'all', + bootFiles => bootFiles.map(entry => entry.default) + ] + + return Promise[ method ]([ + + import(/* webpackMode: "eager" */ 'boot/dayjs') + + ]).then(bootFiles => { + const boot = mapFn(bootFiles).filter(entry => typeof entry === 'function') + start(app, boot) + }) + }) + diff --git a/ui/.quasar/prod-spa/client-prefetch.js b/ui/.quasar/prod-spa/client-prefetch.js new file mode 100644 index 0000000..9bbe3c5 --- /dev/null +++ b/ui/.quasar/prod-spa/client-prefetch.js @@ -0,0 +1,116 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * You are probably looking on adding startup/initialization code. + * Use "quasar new boot " and add it there. + * One boot file per concern. Then reference the file(s) in quasar.config file > boot: + * boot: ['file', ...] // do not add ".js" extension to it. + * + * Boot files are your "main.js" + **/ + + + +import App from 'app/src/App.vue' +let appPrefetch = typeof App.preFetch === 'function' + ? App.preFetch + : ( + // Class components return the component options (and the preFetch hook) inside __c property + App.__c !== void 0 && typeof App.__c.preFetch === 'function' + ? App.__c.preFetch + : false + ) + + +function getMatchedComponents (to, router) { + const route = to + ? (to.matched ? to : router.resolve(to).route) + : router.currentRoute.value + + if (!route) { return [] } + + const matched = route.matched.filter(m => m.components !== void 0) + + if (matched.length === 0) { return [] } + + return Array.prototype.concat.apply([], matched.map(m => { + return Object.keys(m.components).map(key => { + const comp = m.components[key] + return { + path: m.path, + c: comp + } + }) + })) +} + +export function addPreFetchHooks ({ router, store, publicPath }) { + // Add router hook for handling preFetch. + // Doing it after initial route is resolved so that we don't double-fetch + // the data that we already have. Using router.beforeResolve() so that all + // async components are resolved. + router.beforeResolve((to, from, next) => { + const + urlPath = window.location.href.replace(window.location.origin, ''), + matched = getMatchedComponents(to, router), + prevMatched = getMatchedComponents(from, router) + + let diffed = false + const preFetchList = matched + .filter((m, i) => { + return diffed || (diffed = ( + !prevMatched[i] || + prevMatched[i].c !== m.c || + m.path.indexOf('/:') > -1 // does it has params? + )) + }) + .filter(m => m.c !== void 0 && ( + typeof m.c.preFetch === 'function' + // Class components return the component options (and the preFetch hook) inside __c property + || (m.c.__c !== void 0 && typeof m.c.__c.preFetch === 'function') + )) + .map(m => m.c.__c !== void 0 ? m.c.__c.preFetch : m.c.preFetch) + + + if (appPrefetch !== false) { + preFetchList.unshift(appPrefetch) + appPrefetch = false + } + + + if (preFetchList.length === 0) { + return next() + } + + let hasRedirected = false + const redirect = url => { + hasRedirected = true + next(url) + } + const proceed = () => { + + if (hasRedirected === false) { next() } + } + + + + preFetchList.reduce( + (promise, preFetch) => promise.then(() => hasRedirected === false && preFetch({ + store, + currentRoute: to, + previousRoute: from, + redirect, + urlPath, + publicPath + })), + Promise.resolve() + ) + .then(proceed) + .catch(e => { + console.error(e) + proceed() + }) + }) +} diff --git a/ui/.quasar/prod-spa/quasar-user-options.js b/ui/.quasar/prod-spa/quasar-user-options.js new file mode 100644 index 0000000..ac1dae3 --- /dev/null +++ b/ui/.quasar/prod-spa/quasar-user-options.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * You are probably looking on adding startup/initialization code. + * Use "quasar new boot " and add it there. + * One boot file per concern. Then reference the file(s) in quasar.config file > boot: + * boot: ['file', ...] // do not add ".js" extension to it. + * + * Boot files are your "main.js" + **/ + +import lang from 'quasar/lang/tr.js' + + + +import {Loading,Dialog,Notify} from 'quasar' + + + +export default { config: {"notify":{"position":"top","timeout":2500}},lang,plugins: {Loading,Dialog,Notify} } + diff --git a/ui/quasar.config.js.temporary.compiled.1772182016604.mjs b/ui/quasar.config.js.temporary.compiled.1772485482318.mjs similarity index 100% rename from ui/quasar.config.js.temporary.compiled.1772182016604.mjs rename to ui/quasar.config.js.temporary.compiled.1772485482318.mjs diff --git a/ui/src/layouts/MainLayout.vue b/ui/src/layouts/MainLayout.vue index 27386cc..86572ca 100644 --- a/ui/src/layouts/MainLayout.vue +++ b/ui/src/layouts/MainLayout.vue @@ -1,4 +1,4 @@ -