From 9e534e9a34aa14297d89ea6b55a4f79ae2199dd1 Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Fri, 6 Mar 2026 10:56:53 +0300 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- svc/main.go | 101 +- svc/models/statements_params.go | 13 +- svc/queries/statement_aging.go | 221 +-- svc/queries/statement_header.go | 38 +- svc/queries/statements_detail.go | 4 +- svc/routes/customer_balance_pdf.go | 183 +-- svc/routes/statement_aging.go | 46 +- svc/routes/statement_aging_excel.go | 16 +- svc/routes/statement_aging_pdf.go | 549 +------ svc/routes/statement_header.go | 17 +- ...g.js.temporary.compiled.1772783539584.mjs} | 0 ui/src/pages/AccountAgingComparison.vue | 200 +++ ui/src/pages/AccountAgingStatement.vue | 1368 ++++++----------- ui/src/pages/AgingCustomerBalancelist.go.vue | 1148 ++++++++++++++ ui/src/pages/statementofaccount.vue | 190 +-- ui/src/router/routes.js | 2 +- ui/src/stores/accountAgingBalanceStore.js | 2 +- ui/src/stores/statementAgingStore.js | 121 +- 18 files changed, 2204 insertions(+), 2015 deletions(-) rename ui/{quasar.config.js.temporary.compiled.1772696562168.mjs => quasar.config.js.temporary.compiled.1772783539584.mjs} (100%) create mode 100644 ui/src/pages/AccountAgingComparison.vue create mode 100644 ui/src/pages/AgingCustomerBalancelist.go.vue diff --git a/svc/main.go b/svc/main.go index 3c4d306..42f4761 100644 --- a/svc/main.go +++ b/svc/main.go @@ -10,7 +10,6 @@ import ( "bssapp-backend/routes" "database/sql" "log" - "log/slog" "net/http" "os" "path" @@ -165,7 +164,6 @@ InitRoutes — FULL V3 (Method-aware) PERMISSION EDITION func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router { r := mux.NewRouter() - mountUploads(r) mountSPA(r) /* @@ -450,24 +448,21 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "finance", "view", wrapV3(http.HandlerFunc(routes.GetStatementAgingHandler)), ) - - bindV3(r, pgDB, - "/api/finance/account-aging-statement/rebuild-cache", "POST", - "finance", "update", - wrapV3(http.HandlerFunc(routes.RebuildStatementAgingCacheHandler)), - ) - bindV3(r, pgDB, "/api/finance/account-aging-statement/export-pdf", "GET", "finance", "export", wrapV3(routes.ExportStatementAgingPDFHandler(mssql)), ) - bindV3(r, pgDB, "/api/finance/account-aging-statement/export-excel", "GET", "finance", "export", wrapV3(routes.ExportStatementAgingExcelHandler(mssql)), ) + bindV3(r, pgDB, + "/api/finance/aged-customer-balance-list", "GET", + "finance", "view", + wrapV3(http.HandlerFunc(routes.GetAgedCustomerBalanceListHandler)), + ) // ============================================================ // REPORT (STATEMENTS) @@ -508,7 +503,6 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router {"/api/orders/close-ready", "GET", "update", routes.OrderCloseReadyListRoute(mssql)}, {"/api/orders/bulk-close", "POST", "update", routes.OrderBulkCloseRoute(mssql)}, {"/api/orders/export", "GET", "export", routes.OrderListExcelRoute(mssql)}, - {"/api/orders/export-pdf", "GET", "export", routes.OrderListPDFRoute(mssql)}, {"/api/order/check/{id}", "GET", "view", routes.OrderExistsHandler(mssql)}, {"/api/order/validate", "POST", "insert", routes.ValidateOrderHandler(mssql)}, {"/api/order/pdf/{id}", "GET", "export", routes.OrderPDFHandler(mssql)}, @@ -558,36 +552,6 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router wrapV3(http.HandlerFunc(routes.GetProductSecondColorsHandler)), ) - bindV3(r, pgDB, - "/api/product-stock-query", "GET", - "order", "view", - wrapV3(http.HandlerFunc(routes.GetProductStockQueryHandler)), - ) - - bindV3(r, pgDB, - "/api/product-stock-attribute-options", "GET", - "order", "view", - wrapV3(http.HandlerFunc(routes.GetProductStockAttributeOptionsHandler)), - ) - - bindV3(r, pgDB, - "/api/product-stock-query-by-attributes", "GET", - "order", "view", - wrapV3(http.HandlerFunc(routes.GetProductStockQueryByAttributesHandler)), - ) - - bindV3(r, pgDB, - "/api/product-images", "GET", - "order", "view", - wrapV3(routes.GetProductImagesHandler(pgDB)), - ) - - bindV3(r, pgDB, - "/api/product-images/{id}/content", "GET", - "order", "view", - http.HandlerFunc(routes.GetProductImageContentHandler(pgDB)), - ) - // ============================================================ // ROLE MANAGEMENT // ============================================================ @@ -645,35 +609,7 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router return r } -func setupSlog() { - level := new(slog.LevelVar) - switch strings.ToLower(strings.TrimSpace(os.Getenv("LOG_LEVEL"))) { - case "debug": - level.Set(slog.LevelDebug) - case "warn", "warning": - level.Set(slog.LevelWarn) - case "error": - level.Set(slog.LevelError) - default: - level.Set(slog.LevelInfo) - } - - handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ - Level: level, - }) - slog.SetDefault(slog.New(handler)) -} - -func mountUploads(r *mux.Router) { - root := uploadsRootDir() - log.Printf("🖼️ uploads root: %s", root) - r.PathPrefix("/uploads/").Handler( - http.StripPrefix("/uploads/", http.FileServer(http.Dir(root))), - ) -} - func main() { - setupSlog() log.Println("🔥🔥🔥 BSSAPP BACKEND STARTED — LOGIN ROUTE SHOULD EXIST 🔥🔥🔥") // ------------------------------------------------------- @@ -838,30 +774,3 @@ func uiRootDir() string { return "../ui/dist/spa" } - -func uploadsRootDir() string { - if root := strings.TrimSpace(os.Getenv("BLOB_ROOT")); root != "" { - candidates := []string{ - root, - filepath.Join(root, "uploads"), - } - for _, d := range candidates { - if fi, err := os.Stat(d); err == nil && fi.IsDir() { - return d - } - } - } - - candidates := []string{ - "./uploads", - "../uploads", - "../../uploads", - } - for _, d := range candidates { - if fi, err := os.Stat(d); err == nil && fi.IsDir() { - return d - } - } - - return "./uploads" -} diff --git a/svc/models/statements_params.go b/svc/models/statements_params.go index 595363d..ac07b41 100644 --- a/svc/models/statements_params.go +++ b/svc/models/statements_params.go @@ -1,10 +1,11 @@ package models type StatementParams struct { - CariKod string `json:"cari_kod"` - StartDate string `json:"startdate"` - EndDate string `json:"enddate"` - AccountCode string `json:"accountcode"` - LangCode string `json:"langcode"` - Parislemler []string `json:"parislemler"` // ✅ slice olmalı + CariKod string `json:"cari_kod"` + StartDate string `json:"startdate"` + EndDate string `json:"enddate"` + AccountCode string `json:"accountcode"` + LangCode string `json:"langcode"` + Parislemler []string `json:"parislemler"` + ExcludeOpening bool `json:"excludeopening"` } diff --git a/svc/queries/statement_aging.go b/svc/queries/statement_aging.go index 210df28..be2d716 100644 --- a/svc/queries/statement_aging.go +++ b/svc/queries/statement_aging.go @@ -7,7 +7,6 @@ import ( "database/sql" "fmt" "math" - "sort" "strconv" "strings" "time" @@ -15,59 +14,34 @@ import ( func GetStatementAging(params models.StatementAgingParams) ([]map[string]interface{}, error) { accountCode := normalizeMasterAccountCode(params.AccountCode) + if strings.TrimSpace(params.EndDate) == "" { + return nil, fmt.Errorf("enddate is required") + } useType2, useType3 := resolveUseTypes(params.Parislemler) - endDateText := strings.TrimSpace(params.EndDate) - if endDateText == "" { - endDateText = time.Now().Format("2006-01-02") + rateMap, err := loadNearestTryRates(context.Background()) + if err != nil { + return nil, err } - endDate, _ := time.Parse("2006-01-02", endDateText) - - cariFilter := "" - if strings.TrimSpace(accountCode) != "" { - cariFilter = strings.TrimSpace(accountCode) + usdTry := rateMap["USD"] + if usdTry <= 0 { + usdTry = 1 } rows, err := db.MssqlDB.Query(` - SELECT TOP (100) - Cari8 = LEFT(LTRIM(RTRIM(CariKodu)), 8), - CariDetay = LTRIM(RTRIM(CariKodu)), - FaturaCari = LTRIM(RTRIM(CariKodu)), - OdemeCari = LTRIM(RTRIM(CariKodu)), - FaturaRef = CAST(NULL AS NVARCHAR(50)), - OdemeRef = CAST(NULL AS NVARCHAR(50)), - FaturaTarihi = CAST(NULL AS DATE), - OdemeTarihi = CAST(NULL AS DATE), - OdemeDocDate = CAST(NULL AS DATE), - EslesenTutar = CAST(Bakiye AS DECIMAL(18,2)), - GunSayisi = CAST(Vade_Gun AS DECIMAL(18,2)), - GunSayisi_DocDate = CAST(Vade_BelgeTarihi_Gun AS DECIMAL(18,2)), - Aciklama = CAST('AcikKalem' AS NVARCHAR(30)), - DocCurrencyCode = LTRIM(RTRIM(CariDoviz)), - PislemTipi, - SirketKodu, - CurrAccTypeCode, - Bakiye, - Vade_Gun, - Vade_BelgeTarihi_Gun, - SonTarih, - HesaplamaTarihi - FROM dbo.CARI_BAKIYE_GUN_CACHE - WHERE - ( - (@UseType2 = 1 AND PislemTipi = '1_2') - OR - (@UseType3 = 1 AND PislemTipi = '1_3') - ) - AND (@CariFilter = '' OR LTRIM(RTRIM(CariKodu)) LIKE @CariFilter + '%') - ORDER BY CariKodu, CariDoviz, PislemTipi; + 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), - sql.Named("CariFilter", cariFilter), ) if err != nil { - return nil, fmt.Errorf("CARI_BAKIYE_GUN_CACHE query error: %w", err) + return nil, fmt.Errorf("SP_FIFO_MATCH_FINAL query error: %w", err) } defer rows.Close() @@ -78,7 +52,6 @@ func GetStatementAging(params models.StatementAgingParams) ([]map[string]interfa 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)) @@ -108,11 +81,6 @@ func GetStatementAging(params models.StatementAgingParams) ([]map[string]interfa 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) } @@ -125,10 +93,6 @@ func GetStatementAging(params models.StatementAgingParams) ([]map[string]interfa 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] @@ -137,25 +101,13 @@ func GetStatementAging(params models.StatementAgingParams) ([]map[string]interfa 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) + usdTutar := toUSD(tutar, curr, usdTry, rateMap) row["CariDetay"] = cariDetailMap[cari8] row["UsdTutar"] = round2(usdTutar) - row["TryTutar"] = round2(tryTutar) - row["GunKur"] = round6(gunKur) + row["CurrencyTryRate"] = round6(rateMap[curr]) } return result, nil @@ -306,138 +258,3 @@ func round2(v float64) float64 { 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/queries/statement_header.go b/svc/queries/statement_header.go index 861fcab..757b4db 100644 --- a/svc/queries/statement_header.go +++ b/svc/queries/statement_header.go @@ -34,10 +34,38 @@ func GetStatements(ctx context.Context, params models.StatementParams) ([]models } } - piyasaScope, err := buildPiyasaExistsForCariCode(ctx, "b.CurrAccCode") + customerPiyasaInClause, err := resolvePiyasaScopeInClause(ctx, "PF.CustomerAtt01") if err != nil { return nil, err } + vendorPiyasaInClause, err := resolvePiyasaScopeInClause(ctx, "VF.VendorAtt01") + if err != nil { + return nil, err + } + + piyasaScope := fmt.Sprintf(` +( + (b.CurrAccTypeCode = 3 AND EXISTS ( + SELECT 1 + FROM CustomerAttributesFilter PF WITH (NOLOCK) + WHERE (PF.CurrAccCode = b.CurrAccCode OR LEFT(PF.CurrAccCode, 8) = LEFT(b.CurrAccCode, 8)) + AND %s + )) + OR + (b.CurrAccTypeCode = 1 AND EXISTS ( + SELECT 1 + FROM ( + SELECT + CurrAccCode, + VendorAtt01 = MAX(CASE WHEN AttributeTypeCode = 1 THEN AttributeCode END) + FROM prCurrAccAttribute WITH (NOLOCK) + WHERE CurrAccTypeCode = 1 + GROUP BY CurrAccCode + ) VF + WHERE (VF.CurrAccCode = b.CurrAccCode OR LEFT(VF.CurrAccCode, 8) = LEFT(b.CurrAccCode, 8)) + AND %s + )) +)`, customerPiyasaInClause, vendorPiyasaInClause) query := fmt.Sprintf(` ;WITH CurrDesc AS ( @@ -62,7 +90,7 @@ HasMovement AS ( INNER JOIN CurrAccBookATAttributesFilter f ON f.CurrAccBookID = b.CurrAccBookID AND f.ATAtt01 IN (%s) - WHERE LEFT(REPLACE(b.CurrAccCode, ' ', ''), 7) = REPLACE(@Carikod, ' ', '') + WHERE LEFT(REPLACE(b.CurrAccCode, ' ', ''), 7) = LEFT(REPLACE(@Carikod, ' ', ''), 7) AND b.DocumentDate BETWEEN @startdate AND @enddate AND %s ) THEN 1 ELSE 0 END AS HasMov @@ -85,7 +113,8 @@ Opening AS ( LEFT JOIN trCurrAccBookCurrency c ON c.CurrAccBookID = b.CurrAccBookID AND c.CurrencyCode = b.DocCurrencyCode - WHERE LEFT(REPLACE(b.CurrAccCode, ' ', ''), 7) = REPLACE(@Carikod, ' ', '') + WHERE LEFT(REPLACE(b.CurrAccCode, ' ', ''), 7) = LEFT(REPLACE(@Carikod, ' ', ''), 7) + AND @ExcludeOpening = 0 AND %s AND ( (hm.HasMov = 1 AND b.DocumentDate < @startdate) -- hareket varsa: klasik devir @@ -142,7 +171,7 @@ Movements AS ( ON c.CurrAccBookID = b.CurrAccBookID AND c.CurrencyCode = b.DocCurrencyCode - WHERE LEFT(REPLACE(b.CurrAccCode, ' ', ''), 7) = REPLACE(@Carikod, ' ', '') + WHERE LEFT(REPLACE(b.CurrAccCode, ' ', ''), 7) = LEFT(REPLACE(@Carikod, ' ', ''), 7) AND %s AND b.DocumentDate BETWEEN @startdate AND @enddate ) @@ -223,6 +252,7 @@ ORDER BY sql.Named("enddate", params.EndDate), sql.Named("Carikod", params.AccountCode), sql.Named("LangCode", params.LangCode), + sql.Named("ExcludeOpening", params.ExcludeOpening), ) if err != nil { return nil, fmt.Errorf("MSSQL query error: %v", err) diff --git a/svc/queries/statements_detail.go b/svc/queries/statements_detail.go index 34aa9da..8b592db 100644 --- a/svc/queries/statements_detail.go +++ b/svc/queries/statements_detail.go @@ -70,7 +70,7 @@ LEFT JOIN cdItemAttributeDesc KisaKarDesc ON KisaKar.AttributeTypeCode = KisaKarDesc.AttributeTypeCode AND KisaKar.AttributeCode = KisaKarDesc.AttributeCode AND KisaKar.ItemTypeCode = KisaKarDesc.ItemTypeCode -WHERE a.CurrAccCode LIKE @Carikod +WHERE REPLACE(a.CurrAccCode, ' ', '') LIKE REPLACE(@Carikod, ' ', '') + '%%' AND a.InvoiceDate BETWEEN @StartDate AND @EndDate AND %s %s @@ -91,7 +91,7 @@ ORDER BY Belge_Tarihi, Belge_Ref_Numarasi, Urun_Kodu;`, ) rows, err := db.MssqlDB.QueryContext(ctx, query, - sql.Named("Carikod", "%"+accountCode+"%"), + sql.Named("Carikod", normalizeMasterAccountCode(accountCode)), sql.Named("StartDate", startDate), sql.Named("EndDate", endDate), ) diff --git a/svc/routes/customer_balance_pdf.go b/svc/routes/customer_balance_pdf.go index 4c065ab..e0087ad 100644 --- a/svc/routes/customer_balance_pdf.go +++ b/svc/routes/customer_balance_pdf.go @@ -7,9 +7,7 @@ import ( "bytes" "database/sql" "fmt" - "log" "net/http" - "runtime/debug" "sort" "strconv" "strings" @@ -33,18 +31,10 @@ type balanceSummaryPDF struct { TLBakiye13 float64 VadeGun float64 VadeBelge float64 - VadeBase float64 } func ExportCustomerBalancePDFHandler(_ *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - defer func() { - if rec := recover(); rec != nil { - log.Printf("PANIC ExportCustomerBalancePDFHandler: %v\n%s", rec, string(debug.Stack())) - http.Error(w, "internal server error", http.StatusInternalServerError) - } - }() - claims, ok := auth.GetClaimsFromContext(r.Context()) if !ok || claims == nil { http.Error(w, "unauthorized", http.StatusUnauthorized) @@ -147,6 +137,9 @@ func filterCustomerBalanceRowsForPDF(rows []models.CustomerBalanceListRow, exclu func buildCustomerBalancePDFData(rows []models.CustomerBalanceListRow) ([]balanceSummaryPDF, map[string][]models.CustomerBalanceListRow) { summaryMap := make(map[string]*balanceSummaryPDF) detailsByMaster := make(map[string][]models.CustomerBalanceListRow) + vadeWeightMap := make(map[string]float64) + vadeGunSumMap := make(map[string]float64) + vadeBelgeSumMap := make(map[string]float64) for _, row := range rows { master := strings.TrimSpace(row.AnaCariKodu) @@ -194,11 +187,11 @@ func buildCustomerBalancePDFData(rows []models.CustomerBalanceListRow) ([]balanc s.TLBakiye12 += row.TLBakiye12 s.USDBakiye13 += row.USDBakiye13 s.TLBakiye13 += row.TLBakiye13 - w := absFloat(row.Bakiye12) + absFloat(row.Bakiye13) + w := absFloatExcel(row.USDBakiye12) + absFloatExcel(row.TLBakiye12) + absFloatExcel(row.USDBakiye13) + absFloatExcel(row.TLBakiye13) if w > 0 { - s.VadeBase += w - s.VadeGun += row.VadeGun * w - s.VadeBelge += row.VadeBelgeGun * w + vadeWeightMap[master] += w + vadeGunSumMap[master] += row.VadeGun * w + vadeBelgeSumMap[master] += row.VadeBelgeGun * w } detailsByMaster[master] = append(detailsByMaster[master], row) @@ -212,10 +205,9 @@ func buildCustomerBalancePDFData(rows []models.CustomerBalanceListRow) ([]balanc summaries := make([]balanceSummaryPDF, 0, len(masters)) for _, m := range masters { - s := summaryMap[m] - if s != nil && s.VadeBase > 0 { - s.VadeGun = s.VadeGun / s.VadeBase - s.VadeBelge = s.VadeBelge / s.VadeBase + if base := vadeWeightMap[m]; base > 0 { + summaryMap[m].VadeGun = vadeGunSumMap[m] / base + summaryMap[m].VadeBelge = vadeBelgeSumMap[m] / base } summaries = append(summaries, *summaryMap[m]) d := detailsByMaster[m] @@ -248,11 +240,11 @@ func drawCustomerBalancePDF( 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", "Vade Gun", "Belge Gun"} - summaryW := normalizeWidths([]float64{18, 46, 14, 18, 12, 20, 20, 12, 12, 12, 12, 10, 10}, tableW) + 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", "Vade Gun", "Belge Gun"} - detailW := normalizeWidths([]float64{22, 40, 9, 16, 8, 20, 20, 12, 12, 12, 12, 9, 9}, 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() @@ -288,44 +280,6 @@ func drawCustomerBalancePDF( return pdf.GetY()+needH+marginB > 210.0 } - wrappedLines := func(text string, w float64) [][]byte { - t := strings.TrimSpace(sanitizePDFText(text)) - if t == "" { - t = "-" - } - return splitLinesSafe(pdf, t, w) - } - - calcWrappedRowHeight := func(row []string, widths []float64, wrapIdx map[int]bool, lineH float64, minH float64) float64 { - maxLines := 1 - for i, v := range row { - if !wrapIdx[i] { - continue - } - ln := len(wrappedLines(v, widths[i]-2)) - if ln > maxLines { - maxLines = ln - } - } - h := float64(maxLines)*lineH + 1.2 - if h < minH { - return minH - } - return h - } - - drawWrapped := func(text string, x, y, w, rowH, lineH float64, align string) { - lines := wrappedLines(text, w-2) - total := float64(len(lines)) * lineH - startY := y + (rowH-total)/2 - cy := startY - for _, ln := range lines { - pdf.SetXY(x+1, cy) - pdf.CellFormat(w-2, lineH, sanitizePDFText(string(ln)), "", 0, align, false, 0, "") - cy += lineH - } - } - drawSummaryHeader := func() { pdf.SetFont("dejavu", "B", 7.5) pdf.SetFillColor(149, 113, 22) @@ -362,7 +316,12 @@ func drawCustomerBalancePDF( pdf.SetFont("dejavu", "", 7.2) pdf.SetTextColor(20, 20, 20) - drawSummaryRow := func(s balanceSummaryPDF) { + for _, s := range summaries { + if needPage(6.2) { + header() + drawSummaryHeader() + } + row := []string{ s.AnaCariKodu, s.AnaCariAdi, @@ -375,56 +334,31 @@ func drawCustomerBalancePDF( formatMoneyPDF(s.TLBakiye12), formatMoneyPDF(s.USDBakiye13), formatMoneyPDF(s.TLBakiye13), - formatMoneyPDF(s.VadeGun), - formatMoneyPDF(s.VadeBelge), - } - wrapCols := map[int]bool{1: true, 3: true} - rowH := calcWrappedRowHeight(row, summaryW, wrapCols, 3.2, 6.2) - if needPage(rowH) { - header() - drawSummaryHeader() - pdf.SetFont("dejavu", "", 7.2) - pdf.SetTextColor(20, 20, 20) } y := pdf.GetY() x := marginL for i, v := range row { - if detailed { - pdf.SetFillColor(246, 241, 231) - pdf.Rect(x, y, summaryW[i], rowH, "FD") - } else { - pdf.Rect(x, y, summaryW[i], rowH, "") - } + pdf.Rect(x, y, summaryW[i], 6.2, "") align := "L" if i >= 7 { align = "R" } - if wrapCols[i] { - drawWrapped(v, x, y, summaryW[i], rowH, 3.2, "L") - } else { - pdf.SetXY(x+1, y+(rowH-4.2)/2) - pdf.CellFormat(summaryW[i]-2, 4.2, sanitizePDFText(v), "", 0, align, false, 0, "") - } + pdf.SetXY(x+1, y+1) + pdf.CellFormat(summaryW[i]-2, 4.2, v, "", 0, align, false, 0, "") x += summaryW[i] } - pdf.SetY(y + rowH) + pdf.SetY(y + 6.2) } if !detailed { - for _, s := range summaries { - drawSummaryRow(s) - } return } + pdf.Ln(1.8) for _, s := range summaries { - drawSummaryRow(s) - pdf.Ln(1.2) - rows := detailsByMaster[s.AnaCariKodu] if len(rows) == 0 { - pdf.Ln(1.0) continue } @@ -446,25 +380,7 @@ func drawCustomerBalancePDF( pdf.SetTextColor(40, 40, 40) for _, r := range rows { - 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), - formatMoneyPDF(r.VadeGun), - formatMoneyPDF(r.VadeBelgeGun), - } - detailWrapCols := map[int]bool{1: true} - rowH := calcWrappedRowHeight(line, detailW, detailWrapCols, 3.0, 5.8) - - if needPage(rowH) { + if needPage(5.8) { header() pdf.SetFont("dejavu", "B", 8) pdf.SetFillColor(218, 193, 151) @@ -479,23 +395,33 @@ func drawCustomerBalancePDF( 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], rowH, "") + pdf.Rect(rowX, rowY, detailW[i], 5.8, "") align := "L" if i >= 5 { align = "R" } - if detailWrapCols[i] { - drawWrapped(v, rowX, rowY, detailW[i], rowH, 3.0, "L") - } else { - pdf.SetXY(rowX+1, rowY+(rowH-4.0)/2) - pdf.CellFormat(detailW[i]-2, 4.0, sanitizePDFText(v), "", 0, align, false, 0, "") - } + 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 + rowH) + pdf.SetY(rowY + 5.8) } pdf.Ln(1.2) } @@ -548,24 +474,3 @@ func formatMoneyPDF(v float64) string { return sign + strings.Join(out, ".") + "," + decPart } - -func absFloat(v float64) float64 { - if v < 0 { - return -v - } - return v -} - -func sanitizePDFText(s string) string { - s = strings.ToValidUTF8(s, "?") - s = strings.ReplaceAll(s, "\x00", " ") - return strings.Map(func(r rune) rune { - if r == '\n' || r == '\r' || r == '\t' { - return ' ' - } - if r < 32 { - return -1 - } - return r - }, s) -} diff --git a/svc/routes/statement_aging.go b/svc/routes/statement_aging.go index 9a3a04d..9e24046 100644 --- a/svc/routes/statement_aging.go +++ b/svc/routes/statement_aging.go @@ -18,7 +18,51 @@ func GetStatementAgingHandler(w http.ResponseWriter, r *http.Request) { return } - selectedDate := time.Now().Format("2006-01-02") + selectedDate := strings.TrimSpace(r.URL.Query().Get("enddate")) + if selectedDate == "" { + selectedDate = strings.TrimSpace(r.URL.Query().Get("selected_date")) + } + if selectedDate == "" { + selectedDate = time.Now().Format("2006-01-02") + } + params := models.StatementAgingParams{ + AccountCode: strings.TrimSpace(r.URL.Query().Get("accountcode")), + EndDate: selectedDate, + Parislemler: r.URL.Query()["parislemler"], + } + + if err := queries.RebuildStatementAgingCache(r.Context()); err != nil { + http.Error(w, "Error rebuilding aging cache: "+err.Error(), http.StatusInternalServerError) + 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) + } +} + +// GET /api/finance/aged-customer-balance-list +func GetAgedCustomerBalanceListHandler(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("enddate")) + if selectedDate == "" { + selectedDate = strings.TrimSpace(r.URL.Query().Get("selected_date")) + } + if selectedDate == "" { + selectedDate = time.Now().Format("2006-01-02") + } params := models.StatementAgingParams{ AccountCode: strings.TrimSpace(r.URL.Query().Get("accountcode")), EndDate: selectedDate, diff --git a/svc/routes/statement_aging_excel.go b/svc/routes/statement_aging_excel.go index 350edd9..7d21326 100644 --- a/svc/routes/statement_aging_excel.go +++ b/svc/routes/statement_aging_excel.go @@ -21,7 +21,13 @@ func ExportStatementAgingExcelHandler(_ *sql.DB) http.HandlerFunc { return } - selectedDate := time.Now().Format("2006-01-02") + selectedDate := strings.TrimSpace(r.URL.Query().Get("enddate")) + if selectedDate == "" { + 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")), @@ -34,9 +40,17 @@ func ExportStatementAgingExcelHandler(_ *sql.DB) http.HandlerFunc { Il: strings.TrimSpace(r.URL.Query().Get("il")), Ilce: strings.TrimSpace(r.URL.Query().Get("ilce")), } + if accountCode := strings.TrimSpace(r.URL.Query().Get("accountcode")); accountCode != "" { + params.CariSearch = accountCode + } excludeZero12 := parseBoolQuery(r.URL.Query().Get("exclude_zero_12")) excludeZero13 := parseBoolQuery(r.URL.Query().Get("exclude_zero_13")) + if err := queries.RebuildStatementAgingCache(r.Context()); err != nil { + http.Error(w, "Error rebuilding aging cache: "+err.Error(), http.StatusInternalServerError) + return + } + rows, err := queries.GetStatementAgingBalanceList(r.Context(), params) if err != nil { http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError) diff --git a/svc/routes/statement_aging_pdf.go b/svc/routes/statement_aging_pdf.go index e5947bd..fc04c67 100644 --- a/svc/routes/statement_aging_pdf.go +++ b/svc/routes/statement_aging_pdf.go @@ -7,92 +7,30 @@ import ( "bytes" "database/sql" "fmt" - "log" - "math" "net/http" - "runtime/debug" - "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) { - defer func() { - if rec := recover(); rec != nil { - log.Printf("PANIC ExportStatementAgingPDFHandler: %v\n%s", rec, string(debug.Stack())) - http.Error(w, "internal server error", http.StatusInternalServerError) - } - }() - claims, ok := auth.GetClaimsFromContext(r.Context()) if !ok || claims == nil { http.Error(w, "unauthorized", http.StatusUnauthorized) return } - selectedDate := time.Now().Format("2006-01-02") - listParams := models.CustomerBalanceListParams{ + selectedDate := strings.TrimSpace(r.URL.Query().Get("enddate")) + if selectedDate == "" { + 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")), @@ -104,26 +42,31 @@ func ExportStatementAgingPDFHandler(_ *sql.DB) http.HandlerFunc { Il: strings.TrimSpace(r.URL.Query().Get("il")), Ilce: strings.TrimSpace(r.URL.Query().Get("ilce")), } + if accountCode := strings.TrimSpace(r.URL.Query().Get("accountcode")); accountCode != "" { + params.CariSearch = accountCode + } + + if err := queries.RebuildStatementAgingCache(r.Context()); err != nil { + http.Error(w, "Error rebuilding aging cache: "+err.Error(), http.StatusInternalServerError) + return + } + 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")) - if err := queries.RebuildStatementAgingCache(r.Context()); err != nil { - http.Error(w, "cache rebuild error: "+err.Error(), http.StatusInternalServerError) - return - } - - rows, err := queries.GetStatementAgingBalanceList(r.Context(), listParams) + rows, err := queries.GetStatementAgingBalanceList(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, 10) + pdf.SetAutoPageBreak(false, 12) if err := registerDejavuFonts(pdf, "dejavu"); err != nil { http.Error(w, "pdf font error: "+err.Error(), http.StatusInternalServerError) return @@ -132,7 +75,7 @@ func ExportStatementAgingPDFHandler(_ *sql.DB) http.HandlerFunc { drawCustomerBalancePDF( pdf, selectedDate, - listParams.CariSearch, + params.CariSearch, detailed, summaries, detailsByMaster, @@ -149,457 +92,13 @@ func ExportStatementAgingPDFHandler(_ *sql.DB) http.HandlerFunc { return } - w.Header().Set("Content-Type", "application/pdf") filename := "account-aging-summary.pdf" if detailed { filename = "account-aging-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 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, sanitizePDFText("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, sanitizePDFText("Son Tarih: "+p.EndDate), "", 0, "R", false, 0, "") - pdf.SetXY(pageW-marginR-95, marginT+6) - pdf.CellFormat(95, 5, sanitizePDFText("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, sanitizePDFText("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, sanitizePDFText(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, sanitizePDFText(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/svc/routes/statement_header.go b/svc/routes/statement_header.go index 9813016..a25b84a 100644 --- a/svc/routes/statement_header.go +++ b/svc/routes/statement_header.go @@ -6,6 +6,7 @@ import ( "bssapp-backend/queries" "encoding/json" "net/http" + "strconv" ) // GET /api/statements @@ -18,11 +19,17 @@ func GetStatementHeadersHandler(w http.ResponseWriter, r *http.Request) { } params := models.StatementParams{ - StartDate: r.URL.Query().Get("startdate"), - EndDate: r.URL.Query().Get("enddate"), - AccountCode: r.URL.Query().Get("accountcode"), - LangCode: r.URL.Query().Get("langcode"), - Parislemler: r.URL.Query()["parislemler"], + StartDate: r.URL.Query().Get("startdate"), + EndDate: r.URL.Query().Get("enddate"), + AccountCode: r.URL.Query().Get("accountcode"), + LangCode: r.URL.Query().Get("langcode"), + Parislemler: r.URL.Query()["parislemler"], + ExcludeOpening: false, + } + if raw := r.URL.Query().Get("excludeopening"); raw != "" { + if parsed, err := strconv.ParseBool(raw); err == nil { + params.ExcludeOpening = parsed + } } statements, err := queries.GetStatements(r.Context(), params) diff --git a/ui/quasar.config.js.temporary.compiled.1772696562168.mjs b/ui/quasar.config.js.temporary.compiled.1772783539584.mjs similarity index 100% rename from ui/quasar.config.js.temporary.compiled.1772696562168.mjs rename to ui/quasar.config.js.temporary.compiled.1772783539584.mjs diff --git a/ui/src/pages/AccountAgingComparison.vue b/ui/src/pages/AccountAgingComparison.vue new file mode 100644 index 0000000..6b9ee59 --- /dev/null +++ b/ui/src/pages/AccountAgingComparison.vue @@ -0,0 +1,200 @@ + + + diff --git a/ui/src/pages/AccountAgingStatement.vue b/ui/src/pages/AccountAgingStatement.vue index db6ab6e..2b882f9 100644 --- a/ui/src/pages/AccountAgingStatement.vue +++ b/ui/src/pages/AccountAgingStatement.vue @@ -1,454 +1,129 @@ - - - diff --git a/ui/src/pages/AgingCustomerBalancelist.go.vue b/ui/src/pages/AgingCustomerBalancelist.go.vue new file mode 100644 index 0000000..5fef244 --- /dev/null +++ b/ui/src/pages/AgingCustomerBalancelist.go.vue @@ -0,0 +1,1148 @@ +Logo +Konular +Değişiklik İstekleri +Dönüm noktaları +Keşfet +M_Kececi +/ +bssapp +Kod +Konular +Değişiklik İstekleri +İşlemler +Paketler +Projeler +Sürüm +Wiki +Aktivite +Ayarlar +Dosyalar +AccountAgingStatement.vue +ActivityLogs.vue +ChangePassword.vue +CustomerBalanceList.vue +Dashboard.vue +ErrorNotFound.vue +FirstPasswordChange.vue +MainPage.vue +MainPanel.vue +MePassword.vue +OrderBulkClose.vue +OrderEntry.vue +OrderGateway.vue +OrderList.vue +OrderPdf.vue +OrderProductionUpdate.vue +OrderProductionUpdateList.vue +PermissionMatrix.vue +ProductStockByAttributes.vue +ProductStockQuery.vue +ProductionWorker.vue +ProductionWorkerGateway.vue +ResetPassword.vue +RoleDepartmentPermissionGateway.vue +RoleDepartmentPermissionList.vue +RoleDepartmentPermissionPage.vue +StatementHeaderReport.vue +StatementReport.vue +TestMail.vue +UserDetail.vue +UserGateway.vue +UserList.vue +UserPermissionPage.vue +UserSync.vue +dummydata.vue +statementofaccount.vue +App.vue +-.editorconfig +.editorconfig +.env.development +.env.production +.gitignore +.npmrc +README.md +babel.config.js +index.html +jsconfig.json +package-lock.json +package.json +postcss.config.js +quasar.config.js +quasar.config.js.temporary.compiled.1772696562168.mjs +.gitignore +readme.md +bssapp +/ +ui +/ +src +/ +pages +/ +AccountAgingStatement.vue + +M_Kececi +ce31aff645 +Merge remote-tracking branch 'origin/master' +3 gün önce +1059 satır +35 KiB +Vue + + + + + + + diff --git a/ui/src/pages/statementofaccount.vue b/ui/src/pages/statementofaccount.vue index b9b8a92..95c0f6b 100644 --- a/ui/src/pages/statementofaccount.vue +++ b/ui/src/pages/statementofaccount.vue @@ -2,96 +2,104 @@ -
-
-
- -
-
- - - -
-
- - - -
-
- -
-
- -
-
- +
+
+
+ +
+
+ + + +
+
+ + + +
+
+ +
+
+ +
+
+ +
+
+ +
-
@@ -358,6 +366,7 @@ const monetaryTypeOptions = [ { label: '1-3 r hesap', value: ['1', '3'] } ] const selectedMonType = ref(monetaryTypeOptions[0].value) +const excludeOpening = ref(false) /* Expand kontrolü */ const expandedRows = ref({}) @@ -404,7 +413,8 @@ async function onFilterClick() { enddate: dateTo.value, accountcode: selectedCari.value, langcode: 'TR', - parislemler: selectedMonType.value + parislemler: selectedMonType.value, + excludeopening: excludeOpening.value }) await detailStore.loadDetails({ @@ -454,6 +464,7 @@ function resetFilters() { dateFrom.value = '' dateTo.value = '' selectedMonType.value = monetaryTypeOptions[0].value + excludeOpening.value = false statementheaderStore.headers = [] detailStore.reset() } @@ -715,4 +726,3 @@ async function CurrheadDownload() { } } - diff --git a/ui/src/router/routes.js b/ui/src/router/routes.js index c2260a9..562e58a 100644 --- a/ui/src/router/routes.js +++ b/ui/src/router/routes.js @@ -149,7 +149,7 @@ const routes = [ { path: 'aged-customer-balance-list', name: 'aged-customer-balance-list', - redirect: { name: 'account-aging-statement' }, + component: () => import('pages/AgingCustomerBalancelist.go.vue'), meta: { permission: 'finance:view' } }, diff --git a/ui/src/stores/accountAgingBalanceStore.js b/ui/src/stores/accountAgingBalanceStore.js index b02b78c..5e44cad 100644 --- a/ui/src/stores/accountAgingBalanceStore.js +++ b/ui/src/stores/accountAgingBalanceStore.js @@ -180,7 +180,7 @@ export const useAccountAgingBalanceStore = defineStore('accountAgingBalance', { try { this.filters.selectedDate = new Date().toISOString().slice(0, 10) - const { data } = await api.get('/finance/account-aging-statement', { + const { data } = await api.get('/finance/aged-customer-balance-list', { params: { cari_search: String(this.filters.cariSearch || '').trim(), cari_ilk_grup: (this.filters.cariIlkGrup || []).join(','), diff --git a/ui/src/stores/statementAgingStore.js b/ui/src/stores/statementAgingStore.js index ed93e83..959a3fc 100644 --- a/ui/src/stores/statementAgingStore.js +++ b/ui/src/stores/statementAgingStore.js @@ -50,32 +50,25 @@ export const useStatementAgingStore = defineStore('statementAging', { const currencyKey = `${cari8}|${curr}` const tutar = Number(row?.eslesen_tutar) || 0 const usd = Number(row?.usd_tutar) || 0 - const trY = Number(row?.try_tutar) || 0 - const absTry = Math.abs(trY) + const absUsd = Math.abs(usd) const gun = Number(row?.gun_sayisi) || 0 const gunDoc = Number(row?.gun_sayisi_docdate) || 0 const aciklama = String(row?.aciklama || '').toUpperCase() - const isAcik = aciklama === 'ACIKKALEM' if (!masterMap[masterKey]) { masterMap[masterKey] = { group_key: masterKey, cari8: masterKey, cari_detay: String(row?.cari_detay || '').trim(), - acik_kalem_tutari_usd: 0, - acik_kalem_tutari_try: 0, - acik_kalem_ort_vade_gun: 0, - acik_kalem_ort_belge_gun: 0, - normal_usd_tutar: 0, - normal_try_tutar: 0, - ortalama_vade_gun: 0, - ortalama_belge_gun: 0, - weighted_all_base: 0, - weighted_all_gun_sum: 0, - weighted_all_doc_sum: 0, - weighted_open_base: 0, - weighted_open_gun_sum: 0, - weighted_open_doc_sum: 0, + satir_sayisi: 0, + toplam_usd: 0, + normal_usd: 0, + acik_kalem_usd: 0, + weighted_gun_sum: 0, + weighted_gun_doc_sum: 0, + weighted_base: 0, + ortalama_gun: 0, + ortalama_gun_docdate: 0 } } @@ -85,46 +78,48 @@ export const useStatementAgingStore = defineStore('statementAging', { master_key: masterKey, cari8, cari_detay: String(row?.cari_detay || '').trim(), - doviz_cinsi: curr, - acik_kalem_tutari: 0, - acik_kalem_usd: 0, - acik_kalem_try: 0, - ort_gun: 0, - ort_belge_gun: 0, - weighted_open_base: 0, - weighted_open_gun_sum: 0, - weighted_open_doc_sum: 0 + doc_currency_code: curr, + satir_sayisi: 0, + toplam_tutar: 0, + toplam_usd: 0, + normal_tutar: 0, + acik_kalem_tutar: 0, + weighted_gun_sum: 0, + weighted_gun_doc_sum: 0, + weighted_base: 0, + ortalama_gun: 0, + ortalama_gun_docdate: 0 } } const m = masterMap[masterKey] const c = currencyMap[currencyKey] - if (isAcik) { - m.acik_kalem_tutari_usd += usd - m.acik_kalem_tutari_try += trY - c.acik_kalem_tutari += tutar - c.acik_kalem_usd += usd - c.acik_kalem_try += trY + m.satir_sayisi += 1 + m.toplam_usd += usd + if (aciklama === 'ACIKKALEM') { + m.acik_kalem_usd += usd } else { - m.normal_usd_tutar += usd - m.normal_try_tutar += trY + m.normal_usd += usd } - if (absTry > 0) { - m.weighted_all_base += absTry - m.weighted_all_gun_sum += absTry * gun - m.weighted_all_doc_sum += absTry * gunDoc + c.satir_sayisi += 1 + c.toplam_tutar += tutar + c.toplam_usd += usd + if (aciklama === 'ACIKKALEM') { + c.acik_kalem_tutar += tutar + } else { + c.normal_tutar += tutar + } - if (isAcik) { - m.weighted_open_base += absTry - m.weighted_open_gun_sum += absTry * gun - m.weighted_open_doc_sum += absTry * gunDoc + if (absUsd > 0) { + m.weighted_base += absUsd + m.weighted_gun_sum += absUsd * gun + m.weighted_gun_doc_sum += absUsd * gunDoc - c.weighted_open_base += absTry - c.weighted_open_gun_sum += absTry * gun - c.weighted_open_doc_sum += absTry * gunDoc - } + c.weighted_base += absUsd + c.weighted_gun_sum += absUsd * gun + c.weighted_gun_doc_sum += absUsd * gunDoc } if (!detailMap[currencyKey]) detailMap[currencyKey] = [] @@ -134,10 +129,8 @@ export const useStatementAgingStore = defineStore('statementAging', { this.masterRows = Object.values(masterMap) .map((m) => ({ ...m, - acik_kalem_ort_vade_gun: m.weighted_open_base > 0 ? ceilDay(m.weighted_open_gun_sum / m.weighted_open_base) : 0, - acik_kalem_ort_belge_gun: m.weighted_open_base > 0 ? ceilDay(m.weighted_open_doc_sum / m.weighted_open_base) : 0, - ortalama_vade_gun: m.weighted_all_base > 0 ? ceilDay(m.weighted_all_gun_sum / m.weighted_all_base) : 0, - ortalama_belge_gun: m.weighted_all_base > 0 ? ceilDay(m.weighted_all_doc_sum / m.weighted_all_base) : 0 + ortalama_gun: m.weighted_base > 0 ? (m.weighted_gun_sum / m.weighted_base) : 0, + ortalama_gun_docdate: m.weighted_base > 0 ? (m.weighted_gun_doc_sum / m.weighted_base) : 0 })) .sort((a, b) => String(a.cari8).localeCompare(String(b.cari8), 'tr', { sensitivity: 'base' })) @@ -145,32 +138,18 @@ export const useStatementAgingStore = defineStore('statementAging', { for (const c of Object.values(currencyMap)) { const row = { ...c, - ort_gun: c.weighted_open_base > 0 ? ceilDay(c.weighted_open_gun_sum / c.weighted_open_base) : 0, - ort_belge_gun: c.weighted_open_base > 0 ? ceilDay(c.weighted_open_doc_sum / c.weighted_open_base) : 0 + ortalama_gun: c.weighted_base > 0 ? (c.weighted_gun_sum / c.weighted_base) : 0, + ortalama_gun_docdate: c.weighted_base > 0 ? (c.weighted_gun_doc_sum / c.weighted_base) : 0 } if (!currencyByMaster[row.master_key]) currencyByMaster[row.master_key] = [] currencyByMaster[row.master_key].push(row) } for (const key of Object.keys(currencyByMaster)) { - currencyByMaster[key].sort((a, b) => String(a.doviz_cinsi).localeCompare(String(b.doviz_cinsi), 'en', { sensitivity: 'base' })) + currencyByMaster[key].sort((a, b) => String(a.doc_currency_code).localeCompare(String(b.doc_currency_code), 'en', { sensitivity: 'base' })) } this.currencyRowsByMaster = currencyByMaster - for (const key of Object.keys(detailMap)) { - detailMap[key].sort((a, b) => { - const aEmpty = !a?.odeme_tarihi - const bEmpty = !b?.odeme_tarihi - if (aEmpty && !bEmpty) return -1 - if (!aEmpty && bEmpty) return 1 - if (aEmpty && bEmpty) return 0 - const aTs = Date.parse(a.odeme_tarihi) - const bTs = Date.parse(b.odeme_tarihi) - const aNum = Number.isFinite(aTs) ? aTs : -Infinity - const bNum = Number.isFinite(bTs) ? bTs : -Infinity - return bNum - aNum - }) - } this.detailByCurrency = detailMap }, @@ -206,17 +185,9 @@ function normalizeRowKeys(row) { odeme_doc_date: row.OdemeDocDate ?? row.odeme_doc_date ?? null, eslesen_tutar: Number(row.EslesenTutar ?? row.eslesen_tutar ?? 0), usd_tutar: Number(row.UsdTutar ?? row.usd_tutar ?? 0), - try_tutar: Number(row.TryTutar ?? row.try_tutar ?? 0), gun_sayisi: Number(row.GunSayisi ?? row.gun_sayisi ?? 0), gun_sayisi_docdate: Number(row.GunSayisi_DocDate ?? row.gun_sayisi_docdate ?? 0), - gun_kur: Number(row.GunKur ?? row.gun_kur ?? 0), aciklama: row.Aciklama ?? row.aciklama ?? null, doc_currency_code: row.DocCurrencyCode ?? row.doc_currency_code ?? null } } - -function ceilDay(value) { - const n = Number(value) - if (!Number.isFinite(n)) return 0 - return Math.ceil(n) -}