Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-03-06 10:56:53 +03:00
parent 9097b5af2d
commit 9e534e9a34
18 changed files with 2204 additions and 2015 deletions

View File

@@ -10,7 +10,6 @@ import (
"bssapp-backend/routes" "bssapp-backend/routes"
"database/sql" "database/sql"
"log" "log"
"log/slog"
"net/http" "net/http"
"os" "os"
"path" "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 { func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router {
r := mux.NewRouter() r := mux.NewRouter()
mountUploads(r)
mountSPA(r) mountSPA(r)
/* /*
@@ -450,24 +448,21 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
"finance", "view", "finance", "view",
wrapV3(http.HandlerFunc(routes.GetStatementAgingHandler)), 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, bindV3(r, pgDB,
"/api/finance/account-aging-statement/export-pdf", "GET", "/api/finance/account-aging-statement/export-pdf", "GET",
"finance", "export", "finance", "export",
wrapV3(routes.ExportStatementAgingPDFHandler(mssql)), wrapV3(routes.ExportStatementAgingPDFHandler(mssql)),
) )
bindV3(r, pgDB, bindV3(r, pgDB,
"/api/finance/account-aging-statement/export-excel", "GET", "/api/finance/account-aging-statement/export-excel", "GET",
"finance", "export", "finance", "export",
wrapV3(routes.ExportStatementAgingExcelHandler(mssql)), wrapV3(routes.ExportStatementAgingExcelHandler(mssql)),
) )
bindV3(r, pgDB,
"/api/finance/aged-customer-balance-list", "GET",
"finance", "view",
wrapV3(http.HandlerFunc(routes.GetAgedCustomerBalanceListHandler)),
)
// ============================================================ // ============================================================
// REPORT (STATEMENTS) // 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/close-ready", "GET", "update", routes.OrderCloseReadyListRoute(mssql)},
{"/api/orders/bulk-close", "POST", "update", routes.OrderBulkCloseRoute(mssql)}, {"/api/orders/bulk-close", "POST", "update", routes.OrderBulkCloseRoute(mssql)},
{"/api/orders/export", "GET", "export", routes.OrderListExcelRoute(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/check/{id}", "GET", "view", routes.OrderExistsHandler(mssql)},
{"/api/order/validate", "POST", "insert", routes.ValidateOrderHandler(mssql)}, {"/api/order/validate", "POST", "insert", routes.ValidateOrderHandler(mssql)},
{"/api/order/pdf/{id}", "GET", "export", routes.OrderPDFHandler(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)), 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 // ROLE MANAGEMENT
// ============================================================ // ============================================================
@@ -645,35 +609,7 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
return r 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() { func main() {
setupSlog()
log.Println("🔥🔥🔥 BSSAPP BACKEND STARTED — LOGIN ROUTE SHOULD EXIST 🔥🔥🔥") log.Println("🔥🔥🔥 BSSAPP BACKEND STARTED — LOGIN ROUTE SHOULD EXIST 🔥🔥🔥")
// ------------------------------------------------------- // -------------------------------------------------------
@@ -838,30 +774,3 @@ func uiRootDir() string {
return "../ui/dist/spa" 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"
}

View File

@@ -6,5 +6,6 @@ type StatementParams struct {
EndDate string `json:"enddate"` EndDate string `json:"enddate"`
AccountCode string `json:"accountcode"` AccountCode string `json:"accountcode"`
LangCode string `json:"langcode"` LangCode string `json:"langcode"`
Parislemler []string `json:"parislemler"` // ✅ slice olmalı Parislemler []string `json:"parislemler"`
ExcludeOpening bool `json:"excludeopening"`
} }

View File

@@ -7,7 +7,6 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"math" "math"
"sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -15,59 +14,34 @@ import (
func GetStatementAging(params models.StatementAgingParams) ([]map[string]interface{}, error) { func GetStatementAging(params models.StatementAgingParams) ([]map[string]interface{}, error) {
accountCode := normalizeMasterAccountCode(params.AccountCode) accountCode := normalizeMasterAccountCode(params.AccountCode)
if strings.TrimSpace(params.EndDate) == "" {
return nil, fmt.Errorf("enddate is required")
}
useType2, useType3 := resolveUseTypes(params.Parislemler) useType2, useType3 := resolveUseTypes(params.Parislemler)
endDateText := strings.TrimSpace(params.EndDate) rateMap, err := loadNearestTryRates(context.Background())
if endDateText == "" { if err != nil {
endDateText = time.Now().Format("2006-01-02") return nil, err
} }
endDate, _ := time.Parse("2006-01-02", endDateText) usdTry := rateMap["USD"]
if usdTry <= 0 {
cariFilter := "" usdTry = 1
if strings.TrimSpace(accountCode) != "" {
cariFilter = strings.TrimSpace(accountCode)
} }
rows, err := db.MssqlDB.Query(` rows, err := db.MssqlDB.Query(`
SELECT TOP (100) EXEC dbo.SP_FIFO_MATCH_FINAL
Cari8 = LEFT(LTRIM(RTRIM(CariKodu)), 8), @Cari8 = @Cari8,
CariDetay = LTRIM(RTRIM(CariKodu)), @SonTarih = @SonTarih,
FaturaCari = LTRIM(RTRIM(CariKodu)), @UseType2 = @UseType2,
OdemeCari = LTRIM(RTRIM(CariKodu)), @UseType3 = @UseType3;
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;
`, `,
sql.Named("Cari8", accountCode),
sql.Named("SonTarih", params.EndDate),
sql.Named("UseType2", useType2), sql.Named("UseType2", useType2),
sql.Named("UseType3", useType3), sql.Named("UseType3", useType3),
sql.Named("CariFilter", cariFilter),
) )
if err != nil { 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() defer rows.Close()
@@ -78,7 +52,6 @@ func GetStatementAging(params models.StatementAgingParams) ([]map[string]interfa
result := make([]map[string]interface{}, 0, 2048) result := make([]map[string]interface{}, 0, 2048)
cari8Set := make(map[string]struct{}) cari8Set := make(map[string]struct{})
currencySet := make(map[string]struct{})
for rows.Next() { for rows.Next() {
values := make([]interface{}, len(columns)) values := make([]interface{}, len(columns))
scanArgs := make([]interface{}, len(columns)) scanArgs := make([]interface{}, len(columns))
@@ -108,11 +81,6 @@ func GetStatementAging(params models.StatementAgingParams) ([]map[string]interfa
if cari8 != "" { if cari8 != "" {
cari8Set[cari8] = struct{}{} 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) result = append(result, row)
} }
@@ -125,10 +93,6 @@ func GetStatementAging(params models.StatementAgingParams) ([]map[string]interfa
if err != nil { if err != nil {
return nil, err return nil, err
} }
rateSeriesByCurr, err := loadTryRateSeriesByCurrency(context.Background(), currencySet)
if err != nil {
return nil, err
}
for i := range result { for i := range result {
row := result[i] row := result[i]
@@ -137,25 +101,13 @@ func GetStatementAging(params models.StatementAgingParams) ([]map[string]interfa
if curr == "" { if curr == "" {
curr = "TRY" 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"]) tutar := asFloat64(row["EslesenTutar"])
currTry := resolveTryRate(curr, targetDate, rateSeriesByCurr) usdTutar := toUSD(tutar, curr, usdTry, rateMap)
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["CariDetay"] = cariDetailMap[cari8]
row["UsdTutar"] = round2(usdTutar) row["UsdTutar"] = round2(usdTutar)
row["TryTutar"] = round2(tryTutar) row["CurrencyTryRate"] = round6(rateMap[curr])
row["GunKur"] = round6(gunKur)
} }
return result, nil return result, nil
@@ -306,138 +258,3 @@ func round2(v float64) float64 {
func round6(v float64) float64 { func round6(v float64) float64 {
return math.Round(v*1_000_000) / 1_000_000 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 <currency>".
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
}
}

View File

@@ -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 { if err != nil {
return nil, err 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(` query := fmt.Sprintf(`
;WITH CurrDesc AS ( ;WITH CurrDesc AS (
@@ -62,7 +90,7 @@ HasMovement AS (
INNER JOIN CurrAccBookATAttributesFilter f INNER JOIN CurrAccBookATAttributesFilter f
ON f.CurrAccBookID = b.CurrAccBookID ON f.CurrAccBookID = b.CurrAccBookID
AND f.ATAtt01 IN (%s) 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 b.DocumentDate BETWEEN @startdate AND @enddate
AND %s AND %s
) THEN 1 ELSE 0 END AS HasMov ) THEN 1 ELSE 0 END AS HasMov
@@ -85,7 +113,8 @@ Opening AS (
LEFT JOIN trCurrAccBookCurrency c LEFT JOIN trCurrAccBookCurrency c
ON c.CurrAccBookID = b.CurrAccBookID ON c.CurrAccBookID = b.CurrAccBookID
AND c.CurrencyCode = b.DocCurrencyCode 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 %s
AND ( AND (
(hm.HasMov = 1 AND b.DocumentDate < @startdate) -- hareket varsa: klasik devir (hm.HasMov = 1 AND b.DocumentDate < @startdate) -- hareket varsa: klasik devir
@@ -142,7 +171,7 @@ Movements AS (
ON c.CurrAccBookID = b.CurrAccBookID ON c.CurrAccBookID = b.CurrAccBookID
AND c.CurrencyCode = b.DocCurrencyCode 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 %s
AND b.DocumentDate BETWEEN @startdate AND @enddate AND b.DocumentDate BETWEEN @startdate AND @enddate
) )
@@ -223,6 +252,7 @@ ORDER BY
sql.Named("enddate", params.EndDate), sql.Named("enddate", params.EndDate),
sql.Named("Carikod", params.AccountCode), sql.Named("Carikod", params.AccountCode),
sql.Named("LangCode", params.LangCode), sql.Named("LangCode", params.LangCode),
sql.Named("ExcludeOpening", params.ExcludeOpening),
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("MSSQL query error: %v", err) return nil, fmt.Errorf("MSSQL query error: %v", err)

View File

@@ -70,7 +70,7 @@ LEFT JOIN cdItemAttributeDesc KisaKarDesc
ON KisaKar.AttributeTypeCode = KisaKarDesc.AttributeTypeCode ON KisaKar.AttributeTypeCode = KisaKarDesc.AttributeTypeCode
AND KisaKar.AttributeCode = KisaKarDesc.AttributeCode AND KisaKar.AttributeCode = KisaKarDesc.AttributeCode
AND KisaKar.ItemTypeCode = KisaKarDesc.ItemTypeCode 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 a.InvoiceDate BETWEEN @StartDate AND @EndDate
AND %s AND %s
%s %s
@@ -91,7 +91,7 @@ ORDER BY Belge_Tarihi, Belge_Ref_Numarasi, Urun_Kodu;`,
) )
rows, err := db.MssqlDB.QueryContext(ctx, query, rows, err := db.MssqlDB.QueryContext(ctx, query,
sql.Named("Carikod", "%"+accountCode+"%"), sql.Named("Carikod", normalizeMasterAccountCode(accountCode)),
sql.Named("StartDate", startDate), sql.Named("StartDate", startDate),
sql.Named("EndDate", endDate), sql.Named("EndDate", endDate),
) )

View File

@@ -7,9 +7,7 @@ import (
"bytes" "bytes"
"database/sql" "database/sql"
"fmt" "fmt"
"log"
"net/http" "net/http"
"runtime/debug"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -33,18 +31,10 @@ type balanceSummaryPDF struct {
TLBakiye13 float64 TLBakiye13 float64
VadeGun float64 VadeGun float64
VadeBelge float64 VadeBelge float64
VadeBase float64
} }
func ExportCustomerBalancePDFHandler(_ *sql.DB) http.HandlerFunc { func ExportCustomerBalancePDFHandler(_ *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { 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()) claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil { if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized) 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) { func buildCustomerBalancePDFData(rows []models.CustomerBalanceListRow) ([]balanceSummaryPDF, map[string][]models.CustomerBalanceListRow) {
summaryMap := make(map[string]*balanceSummaryPDF) summaryMap := make(map[string]*balanceSummaryPDF)
detailsByMaster := make(map[string][]models.CustomerBalanceListRow) 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 { for _, row := range rows {
master := strings.TrimSpace(row.AnaCariKodu) master := strings.TrimSpace(row.AnaCariKodu)
@@ -194,11 +187,11 @@ func buildCustomerBalancePDFData(rows []models.CustomerBalanceListRow) ([]balanc
s.TLBakiye12 += row.TLBakiye12 s.TLBakiye12 += row.TLBakiye12
s.USDBakiye13 += row.USDBakiye13 s.USDBakiye13 += row.USDBakiye13
s.TLBakiye13 += row.TLBakiye13 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 { if w > 0 {
s.VadeBase += w vadeWeightMap[master] += w
s.VadeGun += row.VadeGun * w vadeGunSumMap[master] += row.VadeGun * w
s.VadeBelge += row.VadeBelgeGun * w vadeBelgeSumMap[master] += row.VadeBelgeGun * w
} }
detailsByMaster[master] = append(detailsByMaster[master], row) detailsByMaster[master] = append(detailsByMaster[master], row)
@@ -212,10 +205,9 @@ func buildCustomerBalancePDFData(rows []models.CustomerBalanceListRow) ([]balanc
summaries := make([]balanceSummaryPDF, 0, len(masters)) summaries := make([]balanceSummaryPDF, 0, len(masters))
for _, m := range masters { for _, m := range masters {
s := summaryMap[m] if base := vadeWeightMap[m]; base > 0 {
if s != nil && s.VadeBase > 0 { summaryMap[m].VadeGun = vadeGunSumMap[m] / base
s.VadeGun = s.VadeGun / s.VadeBase summaryMap[m].VadeBelge = vadeBelgeSumMap[m] / base
s.VadeBelge = s.VadeBelge / s.VadeBase
} }
summaries = append(summaries, *summaryMap[m]) summaries = append(summaries, *summaryMap[m])
d := detailsByMaster[m] d := detailsByMaster[m]
@@ -248,11 +240,11 @@ func drawCustomerBalancePDF(
marginL, marginT, marginR, marginB := 8.0, 8.0, 8.0, 12.0 marginL, marginT, marginR, marginB := 8.0, 8.0, 8.0, 12.0
tableW := pageW - marginL - marginR 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"} 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{18, 46, 14, 18, 12, 20, 20, 12, 12, 12, 12, 10, 10}, tableW) 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"} 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{22, 40, 9, 16, 8, 20, 20, 12, 12, 12, 12, 9, 9}, tableW) detailW := normalizeWidths([]float64{26, 46, 10, 20, 10, 24, 24, 15, 15, 15, 15}, tableW)
header := func() { header := func() {
pdf.AddPage() pdf.AddPage()
@@ -288,44 +280,6 @@ func drawCustomerBalancePDF(
return pdf.GetY()+needH+marginB > 210.0 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() { drawSummaryHeader := func() {
pdf.SetFont("dejavu", "B", 7.5) pdf.SetFont("dejavu", "B", 7.5)
pdf.SetFillColor(149, 113, 22) pdf.SetFillColor(149, 113, 22)
@@ -362,7 +316,12 @@ func drawCustomerBalancePDF(
pdf.SetFont("dejavu", "", 7.2) pdf.SetFont("dejavu", "", 7.2)
pdf.SetTextColor(20, 20, 20) pdf.SetTextColor(20, 20, 20)
drawSummaryRow := func(s balanceSummaryPDF) { for _, s := range summaries {
if needPage(6.2) {
header()
drawSummaryHeader()
}
row := []string{ row := []string{
s.AnaCariKodu, s.AnaCariKodu,
s.AnaCariAdi, s.AnaCariAdi,
@@ -375,56 +334,31 @@ func drawCustomerBalancePDF(
formatMoneyPDF(s.TLBakiye12), formatMoneyPDF(s.TLBakiye12),
formatMoneyPDF(s.USDBakiye13), formatMoneyPDF(s.USDBakiye13),
formatMoneyPDF(s.TLBakiye13), 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() y := pdf.GetY()
x := marginL x := marginL
for i, v := range row { for i, v := range row {
if detailed { pdf.Rect(x, y, summaryW[i], 6.2, "")
pdf.SetFillColor(246, 241, 231)
pdf.Rect(x, y, summaryW[i], rowH, "FD")
} else {
pdf.Rect(x, y, summaryW[i], rowH, "")
}
align := "L" align := "L"
if i >= 7 { if i >= 7 {
align = "R" align = "R"
} }
if wrapCols[i] { pdf.SetXY(x+1, y+1)
drawWrapped(v, x, y, summaryW[i], rowH, 3.2, "L") pdf.CellFormat(summaryW[i]-2, 4.2, v, "", 0, align, false, 0, "")
} else {
pdf.SetXY(x+1, y+(rowH-4.2)/2)
pdf.CellFormat(summaryW[i]-2, 4.2, sanitizePDFText(v), "", 0, align, false, 0, "")
}
x += summaryW[i] x += summaryW[i]
} }
pdf.SetY(y + rowH) pdf.SetY(y + 6.2)
} }
if !detailed { if !detailed {
for _, s := range summaries {
drawSummaryRow(s)
}
return return
} }
pdf.Ln(1.8)
for _, s := range summaries { for _, s := range summaries {
drawSummaryRow(s)
pdf.Ln(1.2)
rows := detailsByMaster[s.AnaCariKodu] rows := detailsByMaster[s.AnaCariKodu]
if len(rows) == 0 { if len(rows) == 0 {
pdf.Ln(1.0)
continue continue
} }
@@ -446,25 +380,7 @@ func drawCustomerBalancePDF(
pdf.SetTextColor(40, 40, 40) pdf.SetTextColor(40, 40, 40)
for _, r := range rows { for _, r := range rows {
line := []string{ if needPage(5.8) {
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) {
header() header()
pdf.SetFont("dejavu", "B", 8) pdf.SetFont("dejavu", "B", 8)
pdf.SetFillColor(218, 193, 151) pdf.SetFillColor(218, 193, 151)
@@ -479,23 +395,33 @@ func drawCustomerBalancePDF(
pdf.SetTextColor(40, 40, 40) 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() rowY := pdf.GetY()
rowX := marginL rowX := marginL
for i, v := range line { for i, v := range line {
pdf.Rect(rowX, rowY, detailW[i], rowH, "") pdf.Rect(rowX, rowY, detailW[i], 5.8, "")
align := "L" align := "L"
if i >= 5 { if i >= 5 {
align = "R" align = "R"
} }
if detailWrapCols[i] { pdf.SetXY(rowX+1, rowY+0.8)
drawWrapped(v, rowX, rowY, detailW[i], rowH, 3.0, "L") pdf.CellFormat(detailW[i]-2, 4.0, v, "", 0, align, false, 0, "")
} else {
pdf.SetXY(rowX+1, rowY+(rowH-4.0)/2)
pdf.CellFormat(detailW[i]-2, 4.0, sanitizePDFText(v), "", 0, align, false, 0, "")
}
rowX += detailW[i] rowX += detailW[i]
} }
pdf.SetY(rowY + rowH) pdf.SetY(rowY + 5.8)
} }
pdf.Ln(1.2) pdf.Ln(1.2)
} }
@@ -548,24 +474,3 @@ func formatMoneyPDF(v float64) string {
return sign + strings.Join(out, ".") + "," + decPart 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)
}

View File

@@ -18,7 +18,51 @@ func GetStatementAgingHandler(w http.ResponseWriter, r *http.Request) {
return 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{ params := models.StatementAgingParams{
AccountCode: strings.TrimSpace(r.URL.Query().Get("accountcode")), AccountCode: strings.TrimSpace(r.URL.Query().Get("accountcode")),
EndDate: selectedDate, EndDate: selectedDate,

View File

@@ -21,7 +21,13 @@ func ExportStatementAgingExcelHandler(_ *sql.DB) http.HandlerFunc {
return 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{ params := models.CustomerBalanceListParams{
SelectedDate: selectedDate, SelectedDate: selectedDate,
CariSearch: strings.TrimSpace(r.URL.Query().Get("cari_search")), 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")), Il: strings.TrimSpace(r.URL.Query().Get("il")),
Ilce: strings.TrimSpace(r.URL.Query().Get("ilce")), 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")) excludeZero12 := parseBoolQuery(r.URL.Query().Get("exclude_zero_12"))
excludeZero13 := parseBoolQuery(r.URL.Query().Get("exclude_zero_13")) 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) rows, err := queries.GetStatementAgingBalanceList(r.Context(), params)
if err != nil { if err != nil {
http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError) http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError)

View File

@@ -7,92 +7,30 @@ import (
"bytes" "bytes"
"database/sql" "database/sql"
"fmt" "fmt"
"log"
"math"
"net/http" "net/http"
"runtime/debug"
"sort"
"strconv"
"strings" "strings"
"time" "time"
"github.com/jung-kurt/gofpdf" "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 { func ExportStatementAgingPDFHandler(_ *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { 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()) claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil { if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized) http.Error(w, "unauthorized", http.StatusUnauthorized)
return return
} }
selectedDate := time.Now().Format("2006-01-02") selectedDate := strings.TrimSpace(r.URL.Query().Get("enddate"))
listParams := models.CustomerBalanceListParams{ 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, SelectedDate: selectedDate,
CariSearch: strings.TrimSpace(r.URL.Query().Get("cari_search")), CariSearch: strings.TrimSpace(r.URL.Query().Get("cari_search")),
CariIlkGrup: strings.TrimSpace(r.URL.Query().Get("cari_ilk_grup")), 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")), Il: strings.TrimSpace(r.URL.Query().Get("il")),
Ilce: strings.TrimSpace(r.URL.Query().Get("ilce")), 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")) detailed := parseBoolQuery(r.URL.Query().Get("detailed"))
excludeZero12 := parseBoolQuery(r.URL.Query().Get("exclude_zero_12")) excludeZero12 := parseBoolQuery(r.URL.Query().Get("exclude_zero_12"))
excludeZero13 := parseBoolQuery(r.URL.Query().Get("exclude_zero_13")) excludeZero13 := parseBoolQuery(r.URL.Query().Get("exclude_zero_13"))
if err := queries.RebuildStatementAgingCache(r.Context()); err != nil { rows, err := queries.GetStatementAgingBalanceList(r.Context(), params)
http.Error(w, "cache rebuild error: "+err.Error(), http.StatusInternalServerError)
return
}
rows, err := queries.GetStatementAgingBalanceList(r.Context(), listParams)
if err != nil { if err != nil {
http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError) http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError)
return return
} }
rows = filterCustomerBalanceRowsForPDF(rows, excludeZero12, excludeZero13) rows = filterCustomerBalanceRowsForPDF(rows, excludeZero12, excludeZero13)
summaries, detailsByMaster := buildCustomerBalancePDFData(rows) summaries, detailsByMaster := buildCustomerBalancePDFData(rows)
pdf := gofpdf.New("L", "mm", "A4", "") pdf := gofpdf.New("L", "mm", "A4", "")
pdf.SetMargins(8, 8, 8) pdf.SetMargins(8, 8, 8)
pdf.SetAutoPageBreak(false, 10) pdf.SetAutoPageBreak(false, 12)
if err := registerDejavuFonts(pdf, "dejavu"); err != nil { if err := registerDejavuFonts(pdf, "dejavu"); err != nil {
http.Error(w, "pdf font error: "+err.Error(), http.StatusInternalServerError) http.Error(w, "pdf font error: "+err.Error(), http.StatusInternalServerError)
return return
@@ -132,7 +75,7 @@ func ExportStatementAgingPDFHandler(_ *sql.DB) http.HandlerFunc {
drawCustomerBalancePDF( drawCustomerBalancePDF(
pdf, pdf,
selectedDate, selectedDate,
listParams.CariSearch, params.CariSearch,
detailed, detailed,
summaries, summaries,
detailsByMaster, detailsByMaster,
@@ -149,457 +92,13 @@ func ExportStatementAgingPDFHandler(_ *sql.DB) http.HandlerFunc {
return return
} }
w.Header().Set("Content-Type", "application/pdf")
filename := "account-aging-summary.pdf" filename := "account-aging-summary.pdf"
if detailed { if detailed {
filename = "account-aging-detailed.pdf" filename = "account-aging-detailed.pdf"
} }
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", filename)) w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", filename))
_, _ = w.Write(buf.Bytes()) _, _ = 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 != "<nil>" {
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
}

View File

@@ -6,6 +6,7 @@ import (
"bssapp-backend/queries" "bssapp-backend/queries"
"encoding/json" "encoding/json"
"net/http" "net/http"
"strconv"
) )
// GET /api/statements // GET /api/statements
@@ -23,6 +24,12 @@ func GetStatementHeadersHandler(w http.ResponseWriter, r *http.Request) {
AccountCode: r.URL.Query().Get("accountcode"), AccountCode: r.URL.Query().Get("accountcode"),
LangCode: r.URL.Query().Get("langcode"), LangCode: r.URL.Query().Get("langcode"),
Parislemler: r.URL.Query()["parislemler"], 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) statements, err := queries.GetStatements(r.Context(), params)

View File

@@ -0,0 +1,200 @@
<template>
<q-page v-if="canReadFinance" class="q-pa-md">
<div class="row q-col-gutter-sm items-end q-mb-md">
<div class="col-12 col-sm-4 col-md-3">
<q-input v-model="accountCode" filled dense label="Cari Kod" clearable />
</div>
<div class="col-12 col-sm-4 col-md-2">
<q-input v-model="endDate" filled dense label="Son Tarih" readonly>
<template #append>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-date v-model="endDate" mask="YYYY-MM-DD" locale="tr-TR" />
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
<div class="col-12 col-sm-4 col-md-3">
<q-select
v-model="parislemler"
:options="parislemOptions"
emit-value
map-options
multiple
filled
dense
label="Parasal İşlem Tipi"
/>
</div>
<div class="col-auto">
<q-btn color="primary" icon="filter_alt" label="Filtrele" :loading="store.loading" @click="load" />
</div>
</div>
<div class="text-caption text-grey-7 q-mb-sm">
Son Tarih bazlı fatura/ödeme karşılaştırma ekranı
</div>
<q-table
:rows="store.masterRows"
:columns="masterColumns"
row-key="group_key"
flat
bordered
dense
:loading="store.loading"
:rows-per-page-options="[25, 50, 100, 0]"
>
<template #body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
size="sm"
dense
flat
round
:icon="expandedMaster[props.row.group_key] ? 'remove' : 'add'"
@click="toggleMaster(props.row.group_key)"
/>
</q-td>
<q-td key="cari8" :props="props">{{ props.row.cari8 }}</q-td>
<q-td key="cari_detay" :props="props">{{ props.row.cari_detay }}</q-td>
<q-td key="acik_kalem_tutari_usd" :props="props" class="text-right">{{ fmt(props.row.acik_kalem_tutari_usd) }}</q-td>
<q-td key="acik_kalem_tutari_try" :props="props" class="text-right">{{ fmt(props.row.acik_kalem_tutari_try) }}</q-td>
<q-td key="acik_kalem_ort_vade_gun" :props="props" class="text-right">{{ fmt(props.row.acik_kalem_ort_vade_gun) }}</q-td>
<q-td key="acik_kalem_ort_belge_gun" :props="props" class="text-right">{{ fmt(props.row.acik_kalem_ort_belge_gun) }}</q-td>
</q-tr>
<q-tr v-if="expandedMaster[props.row.group_key]">
<q-td colspan="100%" class="bg-grey-1">
<q-table
:rows="store.getCurrenciesByMaster(props.row.group_key)"
:columns="currencyColumns"
row-key="group_key"
flat
dense
hide-bottom
>
<template #body="cprops">
<q-tr :props="cprops">
<q-td auto-width>
<q-btn
size="sm"
dense
flat
round
:icon="expandedCurrency[cprops.row.group_key] ? 'remove' : 'add'"
@click="toggleCurrency(cprops.row.group_key)"
/>
</q-td>
<q-td key="doviz_cinsi" :props="cprops">{{ cprops.row.doviz_cinsi }}</q-td>
<q-td key="acik_kalem_tutari" :props="cprops" class="text-right">{{ fmt(cprops.row.acik_kalem_tutari) }}</q-td>
<q-td key="acik_kalem_usd" :props="cprops" class="text-right">{{ fmt(cprops.row.acik_kalem_usd) }}</q-td>
<q-td key="acik_kalem_try" :props="cprops" class="text-right">{{ fmt(cprops.row.acik_kalem_try) }}</q-td>
<q-td key="ort_gun" :props="cprops" class="text-right">{{ fmt(cprops.row.ort_gun) }}</q-td>
<q-td key="ort_belge_gun" :props="cprops" class="text-right">{{ fmt(cprops.row.ort_belge_gun) }}</q-td>
</q-tr>
<q-tr v-if="expandedCurrency[cprops.row.group_key]">
<q-td colspan="100%" class="bg-white">
<q-table
:rows="store.getDetailsByCurrency(cprops.row.group_key)"
:columns="detailColumns"
row-key="detail_key"
flat
dense
hide-bottom
/>
</q-td>
</q-tr>
</template>
</q-table>
</q-td>
</q-tr>
</template>
</q-table>
</q-page>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { useStatementAgingStore } from 'src/stores/statementAgingStore'
import { usePermission } from 'src/composables/usePermission'
const { canRead } = usePermission()
const canReadFinance = canRead('finance')
const store = useStatementAgingStore()
const accountCode = ref('')
const endDate = ref(new Date().toISOString().slice(0, 10))
const parislemler = ref(['2'])
const parislemOptions = [
{ label: '1-2 hesap', value: '2' },
{ label: '1-3 hesap', value: '3' }
]
const expandedMaster = reactive({})
const expandedCurrency = reactive({})
const masterColumns = [
{ name: 'expand', label: '', field: 'expand' },
{ name: 'cari8', label: 'Cari', field: 'cari8', align: 'left' },
{ name: 'cari_detay', label: 'Cari Detay', field: 'cari_detay', align: 'left' },
{ name: 'acik_kalem_tutari_usd', label: 'Açık Kalem USD', field: 'acik_kalem_tutari_usd', align: 'right' },
{ name: 'acik_kalem_tutari_try', label: 'Açık Kalem TRY', field: 'acik_kalem_tutari_try', align: 'right' },
{ name: 'acik_kalem_ort_vade_gun', label: 'Ort Vade Gün', field: 'acik_kalem_ort_vade_gun', align: 'right' },
{ name: 'acik_kalem_ort_belge_gun', label: 'Ort Belge Gün', field: 'acik_kalem_ort_belge_gun', align: 'right' }
]
const currencyColumns = [
{ name: 'expand', label: '', field: 'expand' },
{ name: 'doviz_cinsi', label: 'Döviz', field: 'doviz_cinsi', align: 'left' },
{ name: 'acik_kalem_tutari', label: 'Açık Kalem Tutar', field: 'acik_kalem_tutari', align: 'right' },
{ name: 'acik_kalem_usd', label: 'Açık Kalem USD', field: 'acik_kalem_usd', align: 'right' },
{ name: 'acik_kalem_try', label: 'Açık Kalem TRY', field: 'acik_kalem_try', align: 'right' },
{ name: 'ort_gun', label: 'Ort Gün', field: 'ort_gun', align: 'right' },
{ name: 'ort_belge_gun', label: 'Ort Belge Gün', field: 'ort_belge_gun', align: 'right' }
]
const detailColumns = [
{ name: 'fatura_cari', label: 'Fatura Cari', field: 'fatura_cari', align: 'left' },
{ name: 'odeme_cari', label: 'Ödeme Cari', field: 'odeme_cari', align: 'left' },
{ name: 'fatura_ref', label: 'Fatura Ref', field: 'fatura_ref', align: 'left' },
{ name: 'odeme_ref', label: 'Ödeme Ref', field: 'odeme_ref', align: 'left' },
{ name: 'fatura_tarihi', label: 'Fatura Tarihi', field: 'fatura_tarihi', align: 'left' },
{ name: 'odeme_tarihi', label: 'Ödeme Tarihi', field: 'odeme_tarihi', align: 'left' },
{ name: 'eslesen_tutar', label: 'Eşleşen Tutar', field: 'eslesen_tutar', align: 'right' },
{ name: 'usd_tutar', label: 'USD Tutar', field: 'usd_tutar', align: 'right' },
{ name: 'try_tutar', label: 'TRY Tutar', field: 'try_tutar', align: 'right' },
{ name: 'gun_sayisi', label: 'Gün Sayısı', field: 'gun_sayisi', align: 'right' },
{ name: 'gun_sayisi_docdate', label: 'Belge Gün', field: 'gun_sayisi_docdate', align: 'right' },
{ name: 'gun_kur', label: 'Gün Kur', field: 'gun_kur', align: 'right' },
{ name: 'aciklama', label: 'Açıklama', field: 'aciklama', align: 'left' }
]
function fmt(v) {
const n = Number(v)
if (!Number.isFinite(n)) return v ?? ''
return n.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
function toggleMaster(key) {
expandedMaster[key] = !expandedMaster[key]
}
function toggleCurrency(key) {
expandedCurrency[key] = !expandedCurrency[key]
}
async function load() {
await store.load({
accountcode: accountCode.value || '',
enddate: endDate.value || '',
parislemler: parislemler.value
})
}
load()
</script>

View File

@@ -1,379 +1,68 @@
<template> <template>
<q-page v-if="canReadFinance" class="q-pa-md page-layout"> <q-page v-if="canReadFinance" class="q-px-md q-pb-md q-pt-xs page-col statement-page">
<div class="filter-sticky" :class="{ collapsed: filtersCollapsed }">
<div class="top-actions row q-col-gutter-sm items-end q-mb-sm" :class="{ 'single-line': filtersCollapsed }">
<div class="col-12 col-sm-6 col-md-2">
<q-input
v-model="store.filters.selectedDate"
label="Tarih (Bugün)"
filled
dense
readonly
disable
/>
</div>
<div class="col-12 col-sm-6 col-md-3">
<q-toggle
v-model="store.filters.excludeZeroBalance12"
dense
label="1_2 Bakiyesi Sıfır Olanları Alma"
@update:model-value="onToggle12Changed"
/>
</div>
<div class="col-12 col-sm-6 col-md-3">
<q-toggle
v-model="store.filters.excludeZeroBalance13"
dense
label="1_3 Bakiyesi Sıfır Olanları Alma"
@update:model-value="onToggle13Changed"
/>
</div>
<div class="col-auto">
<q-btn
color="primary"
icon="download"
label="Verileri Yenile"
:loading="store.loading"
@click="store.fetchBalances()"
/>
</div>
<div class="col-auto">
<q-btn
flat
color="grey-8"
icon="restart_alt"
label="Sıfırla"
@click="onReset"
/>
</div>
</div>
<q-slide-transition> <q-slide-transition>
<div v-show="!filtersCollapsed" class="filters-panel q-pa-sm q-mb-md"> <div v-show="!filtersCollapsed" class="local-filter-bar compact-filter q-pa-sm q-mb-xs">
<div class="row q-col-gutter-sm"> <div class="row q-col-gutter-sm items-end">
<div class="col-12 col-sm-6 col-md-4"> <div class="col-12 col-md-5">
<q-input <q-select
v-model="store.filters.cariSearch" v-model="selectedCari"
:options="filteredOptions"
label="Cari kod / isim"
filled filled
dense dense
label="Cari Kodu / Cari Adı" clearable
use-input
input-debounce="300"
@filter="filterCari"
emit-value
map-options
:loading="accountStore.loading"
option-value="value"
option-label="label"
behavior="menu"
:keep-selected="true"
/> />
</div> </div>
<div class="col-12 col-sm-6 col-md-2"> <div class="col-12 col-sm-6 col-md-2">
<q-select <q-input v-model="dateTo" label="Son tarih" filled dense clearable readonly>
v-model="store.filters.cariIlkGrup" <template #append>
:options="store.cariIlkGrupOptions" <q-icon name="event" class="cursor-pointer">
multiple <q-popup-proxy cover transition-show="scale" transition-hide="scale">
emit-value <q-date v-model="dateTo" mask="YYYY-MM-DD" locale="tr-TR" />
map-options </q-popup-proxy>
filled </q-icon>
dense
options-dense
class="compact-select"
label="Cari İlk Grup"
:display-value="selectionLabel(store.filters.cariIlkGrup, 'Cari İlk Grup')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('cariIlkGrup', store.cariIlkGrupOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('cariIlkGrup')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template> </template>
<template #option="scope"> </q-input>
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div> </div>
<div class="col-12 col-sm-6 col-md-3">
<div class="col-12 col-sm-6 col-md-2">
<q-select <q-select
v-model="store.filters.piyasa" v-model="selectedMonType"
:options="store.piyasaOptions" :options="monetaryTypeOptions"
multiple label="Parasal İşlem Tipi"
emit-value emit-value
map-options map-options
filled filled
dense dense
options-dense />
class="compact-select"
label="Piyasa"
:display-value="selectionLabel(store.filters.piyasa, 'Piyasa')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('piyasa', store.piyasaOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('piyasa')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div> </div>
<div class="col-auto">
<div class="col-12 col-sm-6 col-md-2"> <q-btn color="primary" icon="filter_alt" label="Filtrele" @click="onFilterClick" />
<q-select
v-model="store.filters.temsilci"
:options="store.temsilciOptions"
multiple
emit-value
map-options
filled
dense
options-dense
class="compact-select"
label="Temsilci"
:display-value="selectionLabel(store.filters.temsilci, 'Temsilci')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('temsilci', store.temsilciOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('temsilci')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div> </div>
<div class="col-auto">
<div class="col-12 col-sm-6 col-md-2"> <q-btn flat color="grey-8" icon="restart_alt" label="Sıfırla" @click="resetFilters" />
<q-select
v-model="store.filters.riskDurumu"
:options="store.riskDurumuOptions"
multiple
emit-value
map-options
filled
dense
options-dense
class="compact-select"
label="Risk Durumu"
:display-value="selectionLabel(store.filters.riskDurumu, 'Risk Durumu')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('riskDurumu', store.riskDurumuOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('riskDurumu')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-select
v-model="store.filters.islemTipi"
:options="islemTipiOptions"
multiple
emit-value
map-options
filled
dense
options-dense
class="compact-select"
label="İşlem Tipi"
:display-value="selectionLabel(store.filters.islemTipi, 'İşlem Tipi')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('islemTipi', islemTipiOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('islemTipi')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-select
v-model="store.filters.ulke"
:options="store.ulkeOptions"
multiple
emit-value
map-options
filled
dense
options-dense
class="compact-select"
label="Ülke"
:display-value="selectionLabel(store.filters.ulke, 'Ülke')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('ulke', store.ulkeOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('ulke')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-select
v-model="store.filters.il"
:options="store.ilOptions"
multiple
emit-value
map-options
filled
dense
options-dense
class="compact-select"
label="İl"
:display-value="selectionLabel(store.filters.il, 'İl')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('il', store.ilOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('il')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-select
v-model="store.filters.ilce"
:options="store.ilceOptions"
multiple
emit-value
map-options
filled
dense
options-dense
class="compact-select"
label="İlçe"
:display-value="selectionLabel(store.filters.ilce, 'İlçe')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('ilce', store.ilceOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('ilce')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div> </div>
</div> </div>
</div> </div>
</q-slide-transition> </q-slide-transition>
<q-banner v-if="store.error" class="bg-red-1 text-negative q-mb-md rounded-borders"> <div class="table-scroll">
{{ store.error }} <div class="sticky-bar row justify-end items-center q-pa-sm bg-grey-1 q-gutter-sm">
</q-banner>
<q-banner v-if="!store.hasFetched && !store.loading" class="bg-blue-1 text-primary q-mb-md rounded-borders">
Veriler bugünün cache tablosundan okunur. Verileri Yenile ile tekrar yükleyebilirsiniz.
</q-banner>
</div>
<div class="table-area">
<div class="sticky-bar row justify-between items-center q-pa-sm bg-grey-1">
<div />
<div class="row items-center q-gutter-sm">
<q-btn <q-btn
flat flat
color="secondary" color="primary"
icon="list" :icon="filtersCollapsed ? 'unfold_more' : 'unfold_less'"
:label="allDetailsOpen ? 'Tüm Detayları Kapat' : 'Tüm Detayları'" :label="filtersCollapsed ? 'Filtreleri Genişlet' : 'Filtreleri Daralt'"
@click="toggleAllDetails" @click="toggleFiltersCollapsed"
/> />
<q-btn-dropdown <q-btn-dropdown
v-if="canExportFinance" v-if="canExportFinance"
@@ -405,50 +94,36 @@
/> />
<q-btn <q-btn
flat flat
color="primary" color="secondary"
:icon="filtersCollapsed ? 'unfold_more' : 'unfold_less'" icon="list"
:label="filtersCollapsed ? 'Filtreleri Genişlet' : 'Filtreleri Daralt'" :label="allDetailsOpen ? 'Tüm Detayları Kapat' : 'Tüm Detayları'"
@click="toggleFiltersCollapsed" @click="toggleAllDetails"
/> />
</div> </div>
</div>
<q-table <q-table
title="Cari Yaşlandırmalı Cari Bakiye Listesi" class="sticky-table statement-table"
:rows="store.summaryRows" title="Cari Yaşlandırmalı Ekstre"
:columns="summaryColumns" :rows="agingStore.masterRows"
:columns="masterColumns"
row-key="group_key" row-key="group_key"
:loading="store.loading"
flat flat
bordered bordered
dense dense
wrap-cells
separator="cell"
hide-bottom hide-bottom
wrap-cells
:rows-per-page-options="[0]" :rows-per-page-options="[0]"
:pagination="{ rowsPerPage: 0 }" :loading="agingStore.loading"
:table-style="{ tableLayout: 'fixed', width: '100%' }" :table-style="{ tableLayout: 'fixed', width: '100%' }"
class="balance-table"
> >
<template #header="props"> <template #header="props">
<q-tr :props="props" class="header-row"> <q-tr :props="props" class="header-row">
<q-th v-for="col in props.cols" :key="col.name" :props="props"> <q-th v-for="col in props.cols" :key="col.name" :props="props">{{ col.label }}</q-th>
{{ col.label }}
</q-th>
</q-tr>
<q-tr class="totals-row">
<q-th
v-for="col in props.cols"
:key="`tot-${col.name}`"
:class="col.align === 'right' ? 'text-right' : ''"
>
{{ totalCellValue(col.name) }}
</q-th>
</q-tr> </q-tr>
</template> </template>
<template #body="props"> <template #body="props">
<q-tr :props="props" class="sub-header-row"> <q-tr :props="props" class="master-row">
<q-td v-for="col in props.cols" :key="col.name" :props="props"> <q-td v-for="col in props.cols" :key="col.name" :props="props">
<q-btn <q-btn
v-if="col.name === 'expand'" v-if="col.name === 'expand'"
@@ -456,287 +131,301 @@
flat flat
round round
size="sm" size="sm"
:icon="expanded[props.row.group_key] ? 'expand_less' : 'expand_more'" :icon="masterExpanded[props.row.group_key] ? 'expand_less' : 'expand_more'"
@click="toggleGroup(props.row.group_key)" @click="toggleMaster(props.row.group_key)"
/> />
<span v-else-if="col.name === 'prbr_1_2'" class="text-right block prbr-cell"> <span
{{ formatCurrencyMap(props.row.bakiye_1_2_map) }} v-else-if="masterNumericCols.includes(col.name)"
</span> :class="['block', masterCenteredCols.includes(col.name) ? 'text-center' : 'text-right']"
<span v-else-if="col.name === 'prbr_1_3'" class="text-right block prbr-cell"> >
{{ formatCurrencyMap(props.row.bakiye_1_3_map) }}
</span>
<span v-else-if="staticMoneyFields.includes(col.name)" class="text-center block">
{{ formatAmount(props.row[col.field]) }} {{ formatAmount(props.row[col.field]) }}
</span> </span>
<span v-else-if="dayFields.includes(col.name)" class="text-center block"> <span v-else>{{ props.row[col.field] ?? '-' }}</span>
{{ formatDay(props.row[col.field]) }}
</span>
<span v-else>{{ props.row[col.field] || '-' }}</span>
</q-td> </q-td>
</q-tr> </q-tr>
<q-tr v-if="expanded[props.row.group_key]" class="detail-host-row"> <q-tr v-if="masterExpanded[props.row.group_key]" class="master-sub-row">
<q-td colspan="100%"> <q-td colspan="100%" class="q-pa-none">
<div class="detail-wrap"> <div class="currency-groups">
<q-table <div class="currency-level-head">
:rows="store.getDetailsByGroup(props.row.group_key)" <div class="cgh-cell cgh-expand"></div>
:columns="detailColumns" <div class="cgh-cell cgh-code">Döviz</div>
row-key="cari_kodu" <div class="cgh-cell cgh-num">Satır</div>
<div class="cgh-cell cgh-num">Toplam Tutar</div>
<div class="cgh-cell cgh-num">Toplam USD</div>
<div class="cgh-cell cgh-num">Normal</div>
<div class="cgh-cell cgh-num">ık Kalem</div>
<div class="cgh-cell cgh-center">Ort. Gün</div>
<div class="cgh-cell cgh-center">Ort. Gün (DocDate)</div>
</div>
<div
v-for="currRow in agingStore.getCurrenciesByMaster(props.row.group_key)"
:key="currRow.group_key"
class="currency-group"
>
<div class="currency-group-header">
<div class="cgh-cell cgh-expand">
<q-btn
dense dense
flat flat
round
size="sm"
:icon="currencyExpanded[currRow.group_key] ? 'expand_less' : 'expand_more'"
@click="toggleCurrency(currRow.group_key)"
/>
</div>
<div class="cgh-cell cgh-code">{{ currRow.doc_currency_code }}</div>
<div class="cgh-cell cgh-num">{{ formatAmount(currRow.satir_sayisi, 0) }}</div>
<div class="cgh-cell cgh-num">{{ formatAmount(currRow.toplam_tutar) }}</div>
<div class="cgh-cell cgh-num">{{ formatAmount(currRow.toplam_usd) }}</div>
<div class="cgh-cell cgh-num">{{ formatAmount(currRow.normal_tutar) }}</div>
<div class="cgh-cell cgh-num">{{ formatAmount(currRow.acik_kalem_tutar) }}</div>
<div class="cgh-cell cgh-center">{{ formatAmount(currRow.ortalama_gun, 0) }}</div>
<div class="cgh-cell cgh-center">{{ formatAmount(currRow.ortalama_gun_docdate, 0) }}</div>
</div>
<div v-if="currencyExpanded[currRow.group_key]" class="detail-host-row">
<q-table
:rows="agingStore.getDetailsByCurrency(currRow.group_key)"
:columns="detailColumns"
row-key="detail_key"
flat
dense
bordered bordered
hide-bottom hide-bottom
:table-style="{ tableLayout: 'fixed', width: '100%' }" :rows-per-page-options="[0]"
class="detail-table" :pagination="{ rowsPerPage: 0 }"
class="detail-subtable"
:table-style="{ minWidth: '1500px' }"
> >
<template #body-cell-prbr_1_2="scope"> <template #body-cell-eslesen_tutar="d">
<q-td :props="scope" class="text-right prbr-cell"> <q-td :props="d" class="text-right">{{ formatAmount(d.row.eslesen_tutar) }}</q-td>
{{ formatRowPrBr(scope.row, '1_2') }}
</q-td>
</template> </template>
<template #body-cell-prbr_1_3="scope"> <template #body-cell-usd_tutar="d">
<q-td :props="scope" class="text-right prbr-cell"> <q-td :props="d" class="text-right">{{ formatAmount(d.row.usd_tutar) }}</q-td>
{{ formatRowPrBr(scope.row, '1_3') }}
</q-td>
</template> </template>
<template #body-cell-vade_gun="scope"> <template #body-cell-gun_sayisi="d">
<q-td :props="scope" class="text-center"> <q-td :props="d" class="text-center">{{ formatAmount(d.row.gun_sayisi, 0) }}</q-td>
{{ formatDay(scope.row.vade_gun) }}
</q-td>
</template> </template>
<template #body-cell-vade_belge_tarihi_gun="scope"> <template #body-cell-gun_sayisi_docdate="d">
<q-td :props="scope" class="text-center"> <q-td :props="d" class="text-center">{{ formatAmount(d.row.gun_sayisi_docdate, 0) }}</q-td>
{{ formatDay(scope.row.vade_belge_tarihi_gun) }}
</q-td>
</template> </template>
</q-table> </q-table>
</div> </div>
</div>
</div>
</q-td> </q-td>
</q-tr> </q-tr>
</template> </template>
</q-table> </q-table>
</div> </div>
</q-page> </q-page>
<q-page v-else class="q-pa-md flex flex-center"> <q-page v-else class="q-pa-md flex flex-center">
<div class="text-negative text-subtitle1"> <div class="text-negative text-subtitle1">Bu modüle erişim yetkiniz yok.</div>
Bu modüle erişim yetkiniz yok.
</div>
</q-page> </q-page>
</template> </template>
<script setup> <script setup>
import { computed, ref } from 'vue' import { onMounted, ref } from 'vue'
import { useQuasar } from 'quasar' import { useQuasar } from 'quasar'
import { onMounted } from 'vue' import dayjs from 'dayjs'
import { useAccountAgingBalanceStore } from 'src/stores/accountAgingBalanceStore'
import { usePermission } from 'src/composables/usePermission' import { usePermission } from 'src/composables/usePermission'
import { useAccountStore } from 'src/stores/accountStore'
import { useStatementAgingStore } from 'src/stores/statementAgingStore'
import { download, extractApiErrorDetail } from 'src/services/api' import { download, extractApiErrorDetail } from 'src/services/api'
const store = useAccountAgingBalanceStore()
const expanded = ref({})
const allDetailsOpen = ref(false)
const filtersCollapsed = ref(false)
const $q = useQuasar()
const { canRead, canExport } = usePermission() const { canRead, canExport } = usePermission()
const canReadFinance = canRead('finance') const canReadFinance = canRead('finance')
const canExportFinance = canExport('finance') const canExportFinance = canExport('finance')
const islemTipiOptions = [ const $q = useQuasar()
{ label: '1_2 Bakiye Pr.Br', value: 'prbr_1_2' }, const accountStore = useAccountStore()
{ label: '1_3 Bakiye Pr.Br', value: 'prbr_1_3' }, const agingStore = useStatementAgingStore()
{ label: '1_2 USD Bakiye', value: 'usd_1_2' },
{ label: '1_2 TRY Bakiye', value: 'try_1_2' }, const selectedCari = ref(null)
{ label: '1_3 USD Bakiye', value: 'usd_1_3' }, const filteredOptions = ref([])
{ label: '1_3 TRY Bakiye', value: 'try_1_3' } const dateTo = ref(dayjs().format('YYYY-MM-DD'))
const masterExpanded = ref({})
const currencyExpanded = ref({})
const allDetailsOpen = ref(false)
const filtersCollapsed = ref(false)
const monetaryTypeOptions = [
{ label: '1-2 hesap', value: ['1', '2'] },
{ label: '1-3 r hesap', value: ['1', '3'] }
]
const selectedMonType = ref(monetaryTypeOptions[0].value)
const masterColumns = [
{ name: 'expand', label: '', field: 'expand', align: 'center' },
{ name: 'cari8', label: 'Ana Cari Kod', field: 'cari8', align: 'left', sortable: true },
{ name: 'cari_detay', label: 'Ana Cari Detay', field: 'cari_detay', align: 'left', sortable: true },
{ name: 'satir_sayisi', label: 'Satır', field: 'satir_sayisi', align: 'right', sortable: true },
{ name: 'toplam_usd', label: 'Toplam USD', field: 'toplam_usd', align: 'right', sortable: true },
{ name: 'normal_usd', label: 'Normal USD', field: 'normal_usd', align: 'right', sortable: true },
{ name: 'acik_kalem_usd', label: 'Açık Kalem USD', field: 'acik_kalem_usd', align: 'right', sortable: true },
{ name: 'ortalama_gun', label: 'Ort. Gün', field: 'ortalama_gun', align: 'center', sortable: true },
{ name: 'ortalama_gun_docdate', label: 'Ort. Gün (DocDate)', field: 'ortalama_gun_docdate', align: 'center', sortable: true }
] ]
const staticMoneyFields = ['usd_bakiye_1_2', 'tl_bakiye_1_2', 'usd_bakiye_1_3', 'tl_bakiye_1_3'] const detailColumns = [
const dayFields = ['vade_gun', 'vade_belge_tarihi_gun'] { name: 'fatura_cari', label: 'Fatura Cari', field: 'fatura_cari', align: 'left' },
{ name: 'odeme_cari', label: 'Ödeme Cari', field: 'odeme_cari', align: 'left' },
{ name: 'fatura_ref', label: 'Fatura Ref', field: 'fatura_ref', align: 'left' },
{ name: 'odeme_ref', label: 'Ödeme Ref', field: 'odeme_ref', align: 'left' },
{ name: 'fatura_tarihi', label: 'Fatura Tarihi', field: 'fatura_tarihi', align: 'left' },
{ name: 'odeme_tarihi', label: 'Ödeme Vade', field: 'odeme_tarihi', align: 'left' },
{ name: 'odeme_doc_date', label: 'Ödeme DocDate', field: 'odeme_doc_date', align: 'left' },
{ name: 'eslesen_tutar', label: 'Eşleşen Tutar', field: 'eslesen_tutar', align: 'right' },
{ name: 'usd_tutar', label: 'USD Tutar', field: 'usd_tutar', align: 'right' },
{ name: 'gun_sayisi', label: 'Gün', field: 'gun_sayisi', align: 'center' },
{ name: 'gun_sayisi_docdate', label: 'Gün (DocDate)', field: 'gun_sayisi_docdate', align: 'center' },
{ name: 'aciklama', label: 'Açıklama', field: 'aciklama', align: 'left' },
{ name: 'doc_currency_code', label: 'Döviz', field: 'doc_currency_code', align: 'left' }
]
function toNumericSortValue (value) { const masterNumericCols = ['satir_sayisi', 'toplam_usd', 'normal_usd', 'acik_kalem_usd', 'ortalama_gun', 'ortalama_gun_docdate']
if (typeof value === 'number') { const masterCenteredCols = ['ortalama_gun', 'ortalama_gun_docdate']
return Number.isFinite(value) ? value : 0
}
const s = String(value ?? '').trim() function normalizeText(str) {
if (!s) return 0 return (str || '')
.toString()
const hasComma = s.includes(',') .toLocaleLowerCase('tr-TR')
const hasDot = s.includes('.') .normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
let normalized = s.replace(/\s+/g, '') .trim()
if (hasComma && hasDot) {
const lastComma = normalized.lastIndexOf(',')
const lastDot = normalized.lastIndexOf('.')
if (lastComma > lastDot) {
normalized = normalized.replace(/\./g, '').replace(',', '.')
} else {
normalized = normalized.replace(/,/g, '')
}
} else if (hasComma) {
normalized = normalized.replace(/\./g, '').replace(',', '.')
}
const n = Number.parseFloat(normalized)
return Number.isFinite(n) ? n : 0
} }
function sortTextTr (a, b) { function filterCari(val, update) {
return String(a ?? '').localeCompare(String(b ?? ''), 'tr', { sensitivity: 'base' }) const needle = normalizeText(val)
}
const metricDefs = { update(() => {
prbr_1_2: { name: 'prbr_1_2', label: '1_2 Bakiye\nPr.Br', field: 'prbr_1_2', align: 'right', sortable: false }, if (!needle) {
prbr_1_3: { name: 'prbr_1_3', label: '1_3 Bakiye\nPr.Br', field: 'prbr_1_3', align: 'right', sortable: false }, filteredOptions.value = accountStore.accountOptions
usd_1_2: { name: 'usd_bakiye_1_2', label: '1_2 USD_BAKIYE', field: 'usd_bakiye_1_2', align: 'center', sortable: true, sort: (a, b) => toNumericSortValue(a) - toNumericSortValue(b) }, return
try_1_2: { name: 'tl_bakiye_1_2', label: '1_2 TRY_BAKIYE', field: 'tl_bakiye_1_2', align: 'center', sortable: true, sort: (a, b) => toNumericSortValue(a) - toNumericSortValue(b) },
usd_1_3: { name: 'usd_bakiye_1_3', label: '1_3 USD_BAKIYE', field: 'usd_bakiye_1_3', align: 'center', sortable: true, sort: (a, b) => toNumericSortValue(a) - toNumericSortValue(b) },
try_1_3: { name: 'tl_bakiye_1_3', label: '1_3 TRY_BAKIYE', field: 'tl_bakiye_1_3', align: 'center', sortable: true, sort: (a, b) => toNumericSortValue(a) - toNumericSortValue(b) }
}
const selectedMetricKeys = computed(() => {
const selected = store.filters.islemTipi || []
if (!selected.length) return [...Object.keys(metricDefs)]
return selected.filter((k) => k in metricDefs)
})
const summaryColumns = computed(() => ([
{ name: 'expand', label: '', field: 'expand', align: 'center', sortable: false },
{ name: 'ana_cari_kodu', label: 'Ana Cari Kodu', field: 'ana_cari_kodu', align: 'left', sortable: true, sort: sortTextTr },
{ name: 'ana_cari_adi', label: 'Ana Cari Detay', field: 'ana_cari_adi', align: 'left', sortable: true, sort: sortTextTr },
{ name: 'piyasa', label: 'Piyasa', field: 'piyasa', align: 'left', sortable: true, sort: sortTextTr },
{ name: 'temsilci', label: 'Temsilci', field: 'temsilci', align: 'left', sortable: true, sort: sortTextTr },
{ name: 'risk_durumu', label: 'Risk Durumu', field: 'risk_durumu', align: 'left', sortable: true, sort: sortTextTr },
...selectedMetricKeys.value.map((k) => metricDefs[k]),
{ name: 'vade_gun', label: 'Vade Gun', field: 'vade_gun', align: 'center', sortable: true, sort: (a, b) => toNumericSortValue(a) - toNumericSortValue(b) },
{ name: 'vade_belge_tarihi_gun', label: 'Belge Tarihi Gun', field: 'vade_belge_tarihi_gun', align: 'center', sortable: true, sort: (a, b) => toNumericSortValue(a) - toNumericSortValue(b) }
]))
const liveTotals = computed(() => {
return store.filteredRows.reduce((acc, row) => {
acc.usd_bakiye_1_2 += Number(row.usd_bakiye_1_2) || 0
acc.tl_bakiye_1_2 += Number(row.tl_bakiye_1_2) || 0
acc.usd_bakiye_1_3 += Number(row.usd_bakiye_1_3) || 0
acc.tl_bakiye_1_3 += Number(row.tl_bakiye_1_3) || 0
const vadeGun = Number(row.vade_gun) || 0
const vadeBelge = Number(row.vade_belge_tarihi_gun) || 0
if (vadeGun !== 0 || vadeBelge !== 0) {
acc.vade_count += 1
acc.vade_gun_sum += vadeGun
acc.vade_belge_sum += vadeBelge
} }
return acc
}, { filteredOptions.value = accountStore.accountOptions.filter(o => {
usd_bakiye_1_2: 0, const label = normalizeText(o.label)
tl_bakiye_1_2: 0, const value = normalizeText(o.value)
usd_bakiye_1_3: 0, return label.includes(needle) || value.includes(needle)
tl_bakiye_1_3: 0,
vade_gun_sum: 0,
vade_belge_sum: 0,
vade_count: 0
}) })
})
}
onMounted(async () => {
await accountStore.fetchAccounts()
filteredOptions.value = accountStore.accountOptions
}) })
const detailColumns = computed(() => [ async function onFilterClick() {
{ name: 'cari_kodu', label: 'Cari Kodu', field: 'cari_kodu', align: 'left' }, if (!selectedCari.value || !dateTo.value) {
{ name: 'cari_detay', label: 'Cari Detay', field: 'cari_detay', align: 'left' }, $q.notify({
{ name: 'sirket', label: 'Şirket', field: 'sirket', align: 'left' }, type: 'warning',
{ name: 'sirket_detay', label: 'Şirket Detayı', field: 'sirket_detay', align: 'left' }, message: 'Lütfen cari ve son tarih seçiniz.',
{ name: 'muhasebe_kodu', label: 'Muhasebe Kodu', field: 'muhasebe_kodu', align: 'left' }, position: 'top-right'
{ name: 'piyasa', label: 'Piyasa', field: 'piyasa', align: 'left' }, })
{ name: 'temsilci', label: 'Temsilci', field: 'temsilci', align: 'left' },
{ name: 'risk_durumu', label: 'Risk Durumu', field: 'risk_durumu', align: 'left' },
{ name: 'ozellik05', label: 'Ülke', field: 'ozellik05', align: 'left' },
{ name: 'il', label: 'İl', field: 'il', align: 'left' },
{ name: 'ilce', label: 'İlçe', field: 'ilce', align: 'left' },
{ name: 'cari_doviz', label: 'Döviz', field: 'cari_doviz', align: 'left' },
...selectedMetricKeys.value.map((k) => metricDefs[k]),
{ name: 'vade_gun', label: 'Vade Gun', field: 'vade_gun', align: 'center' },
{ name: 'vade_belge_tarihi_gun', label: 'Belge Tarihi Gun', field: 'vade_belge_tarihi_gun', align: 'center' }
])
function onReset () {
store.resetFilters()
store.fetchBalances()
}
function onToggle12Changed (val) {
if (val) {
store.filters.excludeZeroBalance13 = false
}
}
function onToggle13Changed (val) {
if (val) {
store.filters.excludeZeroBalance12 = false
}
}
function toggleFiltersCollapsed () {
filtersCollapsed.value = !filtersCollapsed.value
}
function toggleGroup (key) {
expanded.value[key] = !expanded.value[key]
if (!expanded.value[key]) {
allDetailsOpen.value = false
return
}
allDetailsOpen.value =
store.summaryRows.length > 0 &&
store.summaryRows.every(r => expanded.value[r.group_key])
}
function toggleAllDetails () {
allDetailsOpen.value = !allDetailsOpen.value
if (allDetailsOpen.value) {
const next = {}
for (const row of store.summaryRows) {
next[row.group_key] = true
}
expanded.value = next
return
}
expanded.value = {}
}
async function downloadAgingBalancePDF (detailed) {
if (!canExportFinance.value) {
$q.notify({ type: 'negative', message: 'PDF export yetkiniz yok', position: 'top-right' })
return
}
if (!store.hasFetched) {
$q.notify({ type: 'warning', message: 'Önce Bakiyeleri Getir ile veri yükleyin.', position: 'top-right' })
return return
} }
try { try {
const params = { await agingStore.load({
cari_search: String(store.filters.cariSearch || '').trim(), accountcode: selectedCari.value,
cari_ilk_grup: (store.filters.cariIlkGrup || []).join(','), enddate: dateTo.value,
piyasa: (store.filters.piyasa || []).join(','), parislemler: selectedMonType.value
temsilci: (store.filters.temsilci || []).join(','), })
risk_durumu: (store.filters.riskDurumu || []).join(','),
islem_tipi: (store.filters.islemTipi || []).join(','), const m = {}
ulke: (store.filters.ulke || []).join(','), const c = {}
il: (store.filters.il || []).join(','), for (const row of agingStore.masterRows) {
ilce: (store.filters.ilce || []).join(','), m[row.group_key] = true
exclude_zero_12: store.filters.excludeZeroBalance12 ? '1' : '0', for (const cr of agingStore.getCurrenciesByMaster(row.group_key)) {
exclude_zero_13: store.filters.excludeZeroBalance13 ? '1' : '0', c[cr.group_key] = true
detailed: detailed ? '1' : '0' }
}
masterExpanded.value = m
currencyExpanded.value = c
allDetailsOpen.value = agingStore.masterRows.length > 0
} catch (err) {
const msg = await extractApiErrorDetail(err)
$q.notify({
type: 'negative',
message: msg || 'Veriler yüklenemedi',
position: 'top-right'
})
}
}
function resetFilters() {
selectedCari.value = null
dateTo.value = dayjs().format('YYYY-MM-DD')
selectedMonType.value = monetaryTypeOptions[0].value
masterExpanded.value = {}
currencyExpanded.value = {}
allDetailsOpen.value = false
agingStore.reset()
}
function toggleMaster(key) {
masterExpanded.value[key] = !masterExpanded.value[key]
}
function toggleCurrency(key) {
currencyExpanded.value[key] = !currencyExpanded.value[key]
}
function toggleAllDetails() {
allDetailsOpen.value = !allDetailsOpen.value
if (!allDetailsOpen.value) {
masterExpanded.value = {}
currencyExpanded.value = {}
return
} }
const blob = await download('/finance/account-aging-statement/export-pdf', params) const m = {}
const c = {}
for (const row of agingStore.masterRows) {
m[row.group_key] = true
for (const cr of agingStore.getCurrenciesByMaster(row.group_key)) {
c[cr.group_key] = true
}
}
masterExpanded.value = m
currencyExpanded.value = c
}
function toggleFiltersCollapsed() {
filtersCollapsed.value = !filtersCollapsed.value
}
function buildExportParams(detailed = false) {
return {
accountcode: String(selectedCari.value || '').trim(),
cari_search: String(selectedCari.value || '').trim(),
enddate: dateTo.value,
selected_date: dateTo.value,
parislemler: selectedMonType.value,
detailed: detailed ? '1' : '0',
exclude_zero_12: '0',
exclude_zero_13: '0'
}
}
async function downloadAgingBalancePDF(detailed) {
if (!canExportFinance.value) {
$q.notify({ type: 'negative', message: 'PDF export yetkiniz yok', position: 'top-right' })
return
}
if (!selectedCari.value || !dateTo.value) {
$q.notify({ type: 'warning', message: 'Önce cari ve son tarih seçiniz.', position: 'top-right' })
return
}
try {
const blob = await download('/finance/account-aging-statement/export-pdf', buildExportParams(detailed))
const pdfUrl = window.URL.createObjectURL(new Blob([blob], { type: 'application/pdf' })) const pdfUrl = window.URL.createObjectURL(new Blob([blob], { type: 'application/pdf' }))
window.open(pdfUrl, '_blank') window.open(pdfUrl, '_blank')
} catch (err) { } catch (err) {
@@ -749,37 +438,19 @@ async function downloadAgingBalancePDF (detailed) {
} }
} }
async function downloadAgingBalanceExcel () { async function downloadAgingBalanceExcel() {
if (!canExportFinance.value) { if (!canExportFinance.value) {
$q.notify({ type: 'negative', message: 'Excel export yetkiniz yok', position: 'top-right' }) $q.notify({ type: 'negative', message: 'Excel export yetkiniz yok', position: 'top-right' })
return return
} }
if (!selectedCari.value || !dateTo.value) {
if (!store.hasFetched) { $q.notify({ type: 'warning', message: 'Önce cari ve son tarih seçiniz.', position: 'top-right' })
$q.notify({ type: 'warning', message: 'Önce Bakiyeleri Getir ile veri yükleyin.', position: 'top-right' })
return return
} }
try { try {
const params = { const file = await download('/finance/account-aging-statement/export-excel', buildExportParams(false))
cari_search: String(store.filters.cariSearch || '').trim(), const blob = new Blob([file], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
cari_ilk_grup: (store.filters.cariIlkGrup || []).join(','),
piyasa: (store.filters.piyasa || []).join(','),
temsilci: (store.filters.temsilci || []).join(','),
risk_durumu: (store.filters.riskDurumu || []).join(','),
islem_tipi: (store.filters.islemTipi || []).join(','),
ulke: (store.filters.ulke || []).join(','),
il: (store.filters.il || []).join(','),
ilce: (store.filters.ilce || []).join(','),
exclude_zero_12: store.filters.excludeZeroBalance12 ? '1' : '0',
exclude_zero_13: store.filters.excludeZeroBalance13 ? '1' : '0'
}
const file = await download('/finance/account-aging-statement/export-excel', params)
const blob = new Blob(
[file],
{ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }
)
const url = window.URL.createObjectURL(blob) const url = window.URL.createObjectURL(blob)
const a = document.createElement('a') const a = document.createElement('a')
a.href = url a.href = url
@@ -798,261 +469,224 @@ async function downloadAgingBalanceExcel () {
} }
} }
function formatAmount (value) { function formatAmount(value, fraction = 2) {
const n = Number(value || 0) const n = Number(value || 0)
return new Intl.NumberFormat('tr-TR', { return new Intl.NumberFormat('tr-TR', {
minimumFractionDigits: 2, minimumFractionDigits: fraction,
maximumFractionDigits: 2 maximumFractionDigits: fraction
}).format(n) }).format(n)
} }
function formatDay (value) {
const n = Number(value || 0)
return new Intl.NumberFormat('tr-TR', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(n)
}
function selectionLabel (arr, label) {
const count = Array.isArray(arr) ? arr.length : 0
if (count === 0) return `Tümü (${label})`
if (count === 1) return '1 seçim'
return `${count} seçim`
}
function totalCellValue (colName) {
if (colName === 'expand') return 'Toplam'
if (colName === 'piyasa') return '-'
if (colName === 'temsilci') return '-'
if (colName === 'risk_durumu') return '-'
if (colName === 'prbr_1_2') return formatCurrencyMap(totalByCurrency('1_2'))
if (colName === 'prbr_1_3') return formatCurrencyMap(totalByCurrency('1_3'))
if (colName === 'usd_bakiye_1_2') return formatAmount(liveTotals.value.usd_bakiye_1_2)
if (colName === 'tl_bakiye_1_2') return formatAmount(liveTotals.value.tl_bakiye_1_2)
if (colName === 'usd_bakiye_1_3') return formatAmount(liveTotals.value.usd_bakiye_1_3)
if (colName === 'tl_bakiye_1_3') return formatAmount(liveTotals.value.tl_bakiye_1_3)
if (colName === 'vade_gun') return formatDay(liveTotals.value.vade_count > 0 ? liveTotals.value.vade_gun_sum / liveTotals.value.vade_count : 0)
if (colName === 'vade_belge_tarihi_gun') return formatDay(liveTotals.value.vade_count > 0 ? liveTotals.value.vade_belge_sum / liveTotals.value.vade_count : 0)
return '-'
}
function totalByCurrency (tip) {
const key = tip === '1_2' ? 'bakiye_1_2_map' : 'bakiye_1_3_map'
const out = {}
for (const r of store.summaryRows) {
const m = r[key] || {}
for (const [curr, val] of Object.entries(m)) {
out[curr] = (Number(out[curr]) || 0) + (Number(val) || 0)
}
}
return out
}
function formatCurrencyMap (mapObj) {
const entries = Object.entries(mapObj || {})
.filter(([, amount]) => Number(amount) !== 0)
.sort((a, b) => a[0].localeCompare(b[0], 'en'))
if (!entries.length) return '-'
return entries
.map(([curr, amount]) => `${curr}: ${formatAmount(amount)}`)
.join('\n')
}
function formatRowPrBr (row, tip) {
const curr = String(row.cari_doviz || '').trim().toUpperCase() || 'N/A'
const amount = tip === '1_2'
? (Number(row.bakiye_1_2) || 0)
: (Number(row.bakiye_1_3) || 0)
if (amount === 0) return '-'
return `${curr} ${formatAmount(amount)}`
}
onMounted(async () => {
store.filters.selectedDate = new Date().toISOString().slice(0, 10)
await store.fetchBalances()
})
</script> </script>
<style scoped> <style scoped>
.page-layout { .statement-page {
height: calc(100vh - 110px); height: calc(100vh - 56px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
} }
.filter-sticky { .table-scroll {
position: sticky; flex: 1;
top: 0; min-height: 0;
z-index: 20; overflow: hidden;
background: #fff; display: flex;
padding-bottom: 6px; flex-direction: column;
} }
.filter-sticky.collapsed { .compact-filter {
padding-bottom: 0;
}
.top-actions.single-line {
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
padding-bottom: 4px;
}
.top-actions.single-line > [class*='col-'],
.top-actions.single-line > .col-auto {
flex: 0 0 auto;
min-width: 220px;
}
.top-actions.single-line > .col-auto {
min-width: auto;
}
.filters-panel {
border: 1px solid rgba(0, 0, 0, 0.12); border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 8px; border-radius: 8px;
background: #fafafa; background: #fafafa;
} }
.compact-select :deep(.q-field__control) { .statement-page .local-filter-bar {
min-height: 40px; position: sticky;
} top: 0;
z-index: 40;
.compact-select :deep(.q-field__native),
.compact-select :deep(.q-field__input) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.table-area {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
} }
.sticky-bar { .sticky-bar {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 9; z-index: 30;
flex: 0 0 auto;
background: var(--q-secondary);
color: #fff;
border: 1px solid rgba(0, 0, 0, 0.08);
} }
.balance-table { .statement-table {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
} }
.balance-table :deep(.q-table__container) { .statement-table :deep(.q-table__container) {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.balance-table :deep(.q-table__top) { .statement-table :deep(.q-table__top) {
position: sticky; flex: 0 0 auto;
top: 0; position: static;
z-index: 6;
background: #fff;
} }
.balance-table :deep(.q-table__middle) { .statement-table :deep(.q-table__middle) {
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
overflow-y: auto; overflow: auto !important;
overflow-x: hidden; max-height: none !important;
} }
.balance-table :deep(.header-row th) { .statement-table :deep(.header-row th) {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 8; z-index: 30;
background: var(--q-primary); background: var(--q-primary);
color: #fff; color: #fff;
font-weight: 600; font-weight: 600;
font-family: 'Roboto', sans-serif; border-bottom: 1px solid rgba(255, 255, 255, 0.22);
box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.2), 0 1px 0 rgba(0, 0, 0, 0.2);
} }
.balance-table :deep(.totals-row th) { .statement-table :deep(.master-row td) {
position: sticky; background: color-mix(in srgb, var(--q-secondary) 12%, white);
top: 38px;
z-index: 7;
background: var(--q-secondary);
color: var(--q-dark);
font-weight: 700;
font-family: 'Roboto', sans-serif;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.detail-table :deep(.q-table__middle) {
max-height: 320px;
}
.balance-table :deep(.sub-header-row td) {
background: #fff;
border-bottom: 2px solid rgba(0, 0, 0, 0.18); border-bottom: 2px solid rgba(0, 0, 0, 0.18);
font-weight: 600; font-weight: 600;
} }
.balance-table :deep(.sub-header-row td:first-child) { .statement-table :deep(.master-row td:first-child) {
border-left: 3px solid var(--q-primary); border-left: 3px solid var(--q-primary);
} }
.balance-table :deep(.detail-host-row td) { .statement-table :deep(.master-sub-row td) {
background: #f7f7f7; background: #f4f6fb;
border-bottom: 10px solid #fff; border-bottom: 8px solid #fff;
padding-top: 10px; vertical-align: top;
padding-bottom: 12px; padding: 0 !important;
} }
.detail-wrap { .currency-groups {
border: 1px solid rgba(0, 0, 0, 0.14);
border-left: 4px solid var(--q-secondary);
border-radius: 6px;
background: #fff;
padding: 6px; padding: 6px;
background: #f8faff;
} }
.balance-table :deep(.header-row th) { .currency-group {
white-space: pre-line; border-left: 4px solid var(--q-secondary);
line-height: 1.15; border-top: 1px solid rgba(0, 0, 0, 0.08);
border-right: 1px solid rgba(0, 0, 0, 0.08);
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
margin-bottom: 8px;
background: #fff;
} }
.prbr-cell { .currency-level-head {
white-space: pre-line; position: sticky;
word-break: break-word; top: 36px;
line-height: 1.25; z-index: 26;
display: grid;
grid-template-columns: 48px 100px 80px 1.2fr 1.2fr 1.1fr 1.1fr 110px 140px;
align-items: center;
gap: 0;
background: var(--q-secondary);
color: #fff;
border: 1px solid rgba(0, 0, 0, 0.15);
margin-bottom: 6px;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
} }
.balance-table :deep(th), .currency-group-header {
.balance-table :deep(td), position: sticky;
.detail-table :deep(th), top: 72px;
.detail-table :deep(td) { z-index: 24;
white-space: normal !important; display: grid;
word-break: break-word; grid-template-columns: 48px 100px 80px 1.2fr 1.2fr 1.1fr 1.1fr 110px 140px;
overflow-wrap: anywhere; align-items: center;
gap: 0;
background: #4c5f7a;
color: #fff;
border-bottom: 1px solid rgba(255, 255, 255, 0.18);
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.18);
}
.cgh-cell {
padding: 6px 8px;
border-right: 1px solid rgba(255, 255, 255, 0.2);
font-weight: 600;
font-size: 11px; font-size: 11px;
line-height: 1.2;
padding: 4px 6px !important;
} }
.balance-table :deep(.q-table__table), .cgh-cell:last-child {
.detail-table :deep(.q-table__table) { border-right: none;
width: 100% !important;
} }
.detail-table :deep(.q-table__middle) { .cgh-num {
overflow-x: hidden; text-align: right;
}
.cgh-center {
text-align: center;
}
.cgh-expand {
text-align: center;
}
.detail-host-row :deep(td) {
background: #fdfdfd;
padding: 6px !important;
}
.detail-subtable {
border-left: 4px solid var(--q-primary);
border-top: 1px solid rgba(0, 0, 0, 0.08);
border-right: 1px solid rgba(0, 0, 0, 0.08);
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
background: #fff;
}
.statement-table :deep(th),
.statement-table :deep(td),
.detail-subtable :deep(th),
.detail-subtable :deep(td) {
padding: 3px 6px !important;
font-size: 11px !important;
line-height: 1.2 !important;
}
.detail-subtable :deep(.q-table__top) {
position: static;
}
.detail-subtable :deep(thead th) {
position: sticky;
top: 108px;
z-index: 22;
background: #1f3b5b;
color: #fff;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
}
.detail-subtable :deep(td[data-col="gun_sayisi"]),
.detail-subtable :deep(td[data-col="gun_sayisi_docdate"]),
.statement-table :deep(td[data-col="ortalama_gun"]),
.statement-table :deep(td[data-col="ortalama_gun_docdate"]) {
text-align: center !important;
}
@media (max-width: 1366px) {
.statement-table :deep(th),
.statement-table :deep(td),
.detail-subtable :deep(th),
.detail-subtable :deep(td) {
font-size: 10px !important;
padding: 2px 4px !important;
}
.currency-group-header {
grid-template-columns: 44px 90px 70px 1.1fr 1.1fr 1fr 1fr 90px 120px;
}
} }
</style> </style>

View File

@@ -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
<template>
<q-page v-if="canReadFinance" class="q-pa-md page-layout">
<div class="filter-sticky" :class="{ collapsed: filtersCollapsed }">
<div class="top-actions row q-col-gutter-sm items-end q-mb-sm" :class="{ 'single-line': filtersCollapsed }">
<div class="col-12 col-sm-6 col-md-2">
<q-input
v-model="store.filters.selectedDate"
label="Tarih (Bugün)"
filled
dense
readonly
disable
/>
</div>
<div class="col-12 col-sm-6 col-md-3">
<q-toggle
v-model="store.filters.excludeZeroBalance12"
dense
label="1_2 Bakiyesi Sıfır Olanları Alma"
@update:model-value="onToggle12Changed"
/>
</div>
<div class="col-12 col-sm-6 col-md-3">
<q-toggle
v-model="store.filters.excludeZeroBalance13"
dense
label="1_3 Bakiyesi Sıfır Olanları Alma"
@update:model-value="onToggle13Changed"
/>
</div>
<div class="col-auto">
<q-btn
color="primary"
icon="download"
label="Verileri Yenile"
:loading="store.loading"
@click="store.fetchBalances()"
/>
</div>
<div class="col-auto">
<q-btn
flat
color="grey-8"
icon="restart_alt"
label="Sıfırla"
@click="onReset"
/>
</div>
</div>
<q-slide-transition>
<div v-show="!filtersCollapsed" class="filters-panel q-pa-sm q-mb-md">
<div class="row q-col-gutter-sm">
<div class="col-12 col-sm-6 col-md-4">
<q-input
v-model="store.filters.cariSearch"
filled
dense
label="Cari Kodu / Cari Adı"
/>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-select
v-model="store.filters.cariIlkGrup"
:options="store.cariIlkGrupOptions"
multiple
emit-value
map-options
filled
dense
options-dense
class="compact-select"
label="Cari İlk Grup"
:display-value="selectionLabel(store.filters.cariIlkGrup, 'Cari İlk Grup')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('cariIlkGrup', store.cariIlkGrupOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('cariIlkGrup')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-select
v-model="store.filters.piyasa"
:options="store.piyasaOptions"
multiple
emit-value
map-options
filled
dense
options-dense
class="compact-select"
label="Piyasa"
:display-value="selectionLabel(store.filters.piyasa, 'Piyasa')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('piyasa', store.piyasaOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('piyasa')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-select
v-model="store.filters.temsilci"
:options="store.temsilciOptions"
multiple
emit-value
map-options
filled
dense
options-dense
class="compact-select"
label="Temsilci"
:display-value="selectionLabel(store.filters.temsilci, 'Temsilci')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('temsilci', store.temsilciOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('temsilci')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-select
v-model="store.filters.riskDurumu"
:options="store.riskDurumuOptions"
multiple
emit-value
map-options
filled
dense
options-dense
class="compact-select"
label="Risk Durumu"
:display-value="selectionLabel(store.filters.riskDurumu, 'Risk Durumu')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('riskDurumu', store.riskDurumuOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('riskDurumu')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-select
v-model="store.filters.islemTipi"
:options="islemTipiOptions"
multiple
emit-value
map-options
filled
dense
options-dense
class="compact-select"
label="İşlem Tipi"
:display-value="selectionLabel(store.filters.islemTipi, 'İşlem Tipi')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('islemTipi', islemTipiOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('islemTipi')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-select
v-model="store.filters.ulke"
:options="store.ulkeOptions"
multiple
emit-value
map-options
filled
dense
options-dense
class="compact-select"
label="Ülke"
:display-value="selectionLabel(store.filters.ulke, 'Ülke')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('ulke', store.ulkeOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('ulke')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-select
v-model="store.filters.il"
:options="store.ilOptions"
multiple
emit-value
map-options
filled
dense
options-dense
class="compact-select"
label="İl"
:display-value="selectionLabel(store.filters.il, 'İl')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('il', store.ilOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('il')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-select
v-model="store.filters.ilce"
:options="store.ilceOptions"
multiple
emit-value
map-options
filled
dense
options-dense
class="compact-select"
label="İlçe"
:display-value="selectionLabel(store.filters.ilce, 'İlçe')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('ilce', store.ilceOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('ilce')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
</div>
</div>
</q-slide-transition>
<q-banner v-if="store.error" class="bg-red-1 text-negative q-mb-md rounded-borders">
{{ store.error }}
</q-banner>
<q-banner v-if="!store.hasFetched && !store.loading" class="bg-blue-1 text-primary q-mb-md rounded-borders">
Veriler bugünün cache tablosundan okunur. Verileri Yenile ile tekrar yükleyebilirsiniz.
</q-banner>
</div>
<div class="table-area">
<div class="sticky-bar row justify-between items-center q-pa-sm bg-grey-1">
<div />
<div class="row items-center q-gutter-sm">
<q-btn
flat
color="secondary"
icon="list"
:label="allDetailsOpen ? 'Tüm Detayları Kapat' : 'Tüm Detayları Aç'"
@click="toggleAllDetails"
/>
<q-btn-dropdown
v-if="canExportFinance"
flat
color="red"
icon="picture_as_pdf"
label="Yazdır"
>
<q-list style="min-width: 240px">
<q-item clickable v-close-popup @click="downloadAgingBalancePDF(true)">
<q-item-section class="text-primary">
Detaylı Cari Yaşlandırmalı Bakiye Listesi Yazdır
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="downloadAgingBalancePDF(false)">
<q-item-section class="text-secondary">
Detaysız Cari Yaşlandırmalı Bakiye Listesi Yazdır
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<q-btn
v-if="canExportFinance"
flat
color="green-8"
icon="table_view"
label="Excel"
@click="downloadAgingBalanceExcel"
/>
<q-btn
flat
color="primary"
:icon="filtersCollapsed ? 'unfold_more' : 'unfold_less'"
:label="filtersCollapsed ? 'Filtreleri Genişlet' : 'Filtreleri Daralt'"
@click="toggleFiltersCollapsed"
/>
</div>
</div>
<q-table
title="Cari Yaşlandırmalı Cari Bakiye Listesi"
:rows="store.summaryRows"
:columns="summaryColumns"
row-key="group_key"
:loading="store.loading"
flat
bordered
dense
wrap-cells
separator="cell"
hide-bottom
:rows-per-page-options="[0]"
:pagination="{ rowsPerPage: 0 }"
:table-style="{ tableLayout: 'fixed', width: '100%' }"
class="balance-table"
>
<template #header="props">
<q-tr :props="props" class="header-row">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
<q-tr class="totals-row">
<q-th
v-for="col in props.cols"
:key="`tot-${col.name}`"
:class="col.align === 'right' ? 'text-right' : ''"
>
{{ totalCellValue(col.name) }}
</q-th>
</q-tr>
</template>
<template #body="props">
<q-tr :props="props" class="sub-header-row">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
<q-btn
v-if="col.name === 'expand'"
dense
flat
round
size="sm"
:icon="expanded[props.row.group_key] ? 'expand_less' : 'expand_more'"
@click="toggleGroup(props.row.group_key)"
/>
<span v-else-if="col.name === 'prbr_1_2'" class="text-right block prbr-cell">
{{ formatCurrencyMap(props.row.bakiye_1_2_map) }}
</span>
<span v-else-if="col.name === 'prbr_1_3'" class="text-right block prbr-cell">
{{ formatCurrencyMap(props.row.bakiye_1_3_map) }}
</span>
<span v-else-if="staticMoneyFields.includes(col.name)" class="text-center block">
{{ formatAmount(props.row[col.field]) }}
</span>
<span v-else-if="dayFields.includes(col.name)" class="text-center block">
{{ formatDay(props.row[col.field]) }}
</span>
<span v-else>{{ props.row[col.field] || '-' }}</span>
</q-td>
</q-tr>
<q-tr v-if="expanded[props.row.group_key]" class="detail-host-row">
<q-td colspan="100%">
<div class="detail-wrap">
<q-table
:rows="store.getDetailsByGroup(props.row.group_key)"
:columns="detailColumns"
row-key="cari_kodu"
dense
flat
bordered
hide-bottom
:table-style="{ tableLayout: 'fixed', width: '100%' }"
class="detail-table"
>
<template #body-cell-prbr_1_2="scope">
<q-td :props="scope" class="text-right prbr-cell">
{{ formatRowPrBr(scope.row, '1_2') }}
</q-td>
</template>
<template #body-cell-prbr_1_3="scope">
<q-td :props="scope" class="text-right prbr-cell">
{{ formatRowPrBr(scope.row, '1_3') }}
</q-td>
</template>
<template #body-cell-vade_gun="scope">
<q-td :props="scope" class="text-center">
{{ formatDay(scope.row.vade_gun) }}
</q-td>
</template>
<template #body-cell-vade_belge_tarihi_gun="scope">
<q-td :props="scope" class="text-center">
{{ formatDay(scope.row.vade_belge_tarihi_gun) }}
</q-td>
</template>
</q-table>
</div>
</q-td>
</q-tr>
</template>
</q-table>
</div>
</q-page>
<q-page v-else class="q-pa-md flex flex-center">
<div class="text-negative text-subtitle1">
Bu modüle erişim yetkiniz yok.
</div>
</q-page>
</template>
<script setup>
import { computed, ref } from 'vue'
import { useQuasar } from 'quasar'
import { onMounted } from 'vue'
import { useAccountAgingBalanceStore } from 'src/stores/accountAgingBalanceStore'
import { usePermission } from 'src/composables/usePermission'
import { download, extractApiErrorDetail } from 'src/services/api'
const store = useAccountAgingBalanceStore()
const expanded = ref({})
const allDetailsOpen = ref(false)
const filtersCollapsed = ref(false)
const $q = useQuasar()
const { canRead, canExport } = usePermission()
const canReadFinance = canRead('finance')
const canExportFinance = canExport('finance')
const islemTipiOptions = [
{ label: '1_2 Bakiye Pr.Br', value: 'prbr_1_2' },
{ label: '1_3 Bakiye Pr.Br', value: 'prbr_1_3' },
{ label: '1_2 USD Bakiye', value: 'usd_1_2' },
{ label: '1_2 TRY Bakiye', value: 'try_1_2' },
{ label: '1_3 USD Bakiye', value: 'usd_1_3' },
{ label: '1_3 TRY Bakiye', value: 'try_1_3' }
]
const staticMoneyFields = ['usd_bakiye_1_2', 'tl_bakiye_1_2', 'usd_bakiye_1_3', 'tl_bakiye_1_3']
const dayFields = ['vade_gun', 'vade_belge_tarihi_gun']
function toNumericSortValue (value) {
if (typeof value === 'number') {
return Number.isFinite(value) ? value : 0
}
const s = String(value ?? '').trim()
if (!s) return 0
const hasComma = s.includes(',')
const hasDot = s.includes('.')
let normalized = s.replace(/\s+/g, '')
if (hasComma && hasDot) {
const lastComma = normalized.lastIndexOf(',')
const lastDot = normalized.lastIndexOf('.')
if (lastComma > lastDot) {
normalized = normalized.replace(/\./g, '').replace(',', '.')
} else {
normalized = normalized.replace(/,/g, '')
}
} else if (hasComma) {
normalized = normalized.replace(/\./g, '').replace(',', '.')
}
const n = Number.parseFloat(normalized)
return Number.isFinite(n) ? n : 0
}
function sortTextTr (a, b) {
return String(a ?? '').localeCompare(String(b ?? ''), 'tr', { sensitivity: 'base' })
}
const metricDefs = {
prbr_1_2: { name: 'prbr_1_2', label: '1_2 Bakiye\nPr.Br', field: 'prbr_1_2', align: 'right', sortable: false },
prbr_1_3: { name: 'prbr_1_3', label: '1_3 Bakiye\nPr.Br', field: 'prbr_1_3', align: 'right', sortable: false },
usd_1_2: { name: 'usd_bakiye_1_2', label: '1_2 USD_BAKIYE', field: 'usd_bakiye_1_2', align: 'center', sortable: true, sort: (a, b) => toNumericSortValue(a) - toNumericSortValue(b) },
try_1_2: { name: 'tl_bakiye_1_2', label: '1_2 TRY_BAKIYE', field: 'tl_bakiye_1_2', align: 'center', sortable: true, sort: (a, b) => toNumericSortValue(a) - toNumericSortValue(b) },
usd_1_3: { name: 'usd_bakiye_1_3', label: '1_3 USD_BAKIYE', field: 'usd_bakiye_1_3', align: 'center', sortable: true, sort: (a, b) => toNumericSortValue(a) - toNumericSortValue(b) },
try_1_3: { name: 'tl_bakiye_1_3', label: '1_3 TRY_BAKIYE', field: 'tl_bakiye_1_3', align: 'center', sortable: true, sort: (a, b) => toNumericSortValue(a) - toNumericSortValue(b) }
}
const selectedMetricKeys = computed(() => {
const selected = store.filters.islemTipi || []
if (!selected.length) return [...Object.keys(metricDefs)]
return selected.filter((k) => k in metricDefs)
})
const summaryColumns = computed(() => ([
{ name: 'expand', label: '', field: 'expand', align: 'center', sortable: false },
{ name: 'ana_cari_kodu', label: 'Ana Cari Kodu', field: 'ana_cari_kodu', align: 'left', sortable: true, sort: sortTextTr },
{ name: 'ana_cari_adi', label: 'Ana Cari Detay', field: 'ana_cari_adi', align: 'left', sortable: true, sort: sortTextTr },
{ name: 'piyasa', label: 'Piyasa', field: 'piyasa', align: 'left', sortable: true, sort: sortTextTr },
{ name: 'temsilci', label: 'Temsilci', field: 'temsilci', align: 'left', sortable: true, sort: sortTextTr },
{ name: 'risk_durumu', label: 'Risk Durumu', field: 'risk_durumu', align: 'left', sortable: true, sort: sortTextTr },
...selectedMetricKeys.value.map((k) => metricDefs[k]),
{ name: 'vade_gun', label: 'Vade Gun', field: 'vade_gun', align: 'center', sortable: true, sort: (a, b) => toNumericSortValue(a) - toNumericSortValue(b) },
{ name: 'vade_belge_tarihi_gun', label: 'Belge Tarihi Gun', field: 'vade_belge_tarihi_gun', align: 'center', sortable: true, sort: (a, b) => toNumericSortValue(a) - toNumericSortValue(b) }
]))
const liveTotals = computed(() => {
return store.filteredRows.reduce((acc, row) => {
acc.usd_bakiye_1_2 += Number(row.usd_bakiye_1_2) || 0
acc.tl_bakiye_1_2 += Number(row.tl_bakiye_1_2) || 0
acc.usd_bakiye_1_3 += Number(row.usd_bakiye_1_3) || 0
acc.tl_bakiye_1_3 += Number(row.tl_bakiye_1_3) || 0
const vadeGun = Number(row.vade_gun) || 0
const vadeBelge = Number(row.vade_belge_tarihi_gun) || 0
if (vadeGun !== 0 || vadeBelge !== 0) {
acc.vade_count += 1
acc.vade_gun_sum += vadeGun
acc.vade_belge_sum += vadeBelge
}
return acc
}, {
usd_bakiye_1_2: 0,
tl_bakiye_1_2: 0,
usd_bakiye_1_3: 0,
tl_bakiye_1_3: 0,
vade_gun_sum: 0,
vade_belge_sum: 0,
vade_count: 0
})
})
const detailColumns = computed(() => [
{ name: 'cari_kodu', label: 'Cari Kodu', field: 'cari_kodu', align: 'left' },
{ name: 'cari_detay', label: 'Cari Detay', field: 'cari_detay', align: 'left' },
{ name: 'sirket', label: 'Şirket', field: 'sirket', align: 'left' },
{ name: 'sirket_detay', label: 'Şirket Detayı', field: 'sirket_detay', align: 'left' },
{ name: 'muhasebe_kodu', label: 'Muhasebe Kodu', field: 'muhasebe_kodu', align: 'left' },
{ name: 'piyasa', label: 'Piyasa', field: 'piyasa', align: 'left' },
{ name: 'temsilci', label: 'Temsilci', field: 'temsilci', align: 'left' },
{ name: 'risk_durumu', label: 'Risk Durumu', field: 'risk_durumu', align: 'left' },
{ name: 'ozellik05', label: 'Ülke', field: 'ozellik05', align: 'left' },
{ name: 'il', label: 'İl', field: 'il', align: 'left' },
{ name: 'ilce', label: 'İlçe', field: 'ilce', align: 'left' },
{ name: 'cari_doviz', label: 'Döviz', field: 'cari_doviz', align: 'left' },
...selectedMetricKeys.value.map((k) => metricDefs[k]),
{ name: 'vade_gun', label: 'Vade Gun', field: 'vade_gun', align: 'center' },
{ name: 'vade_belge_tarihi_gun', label: 'Belge Tarihi Gun', field: 'vade_belge_tarihi_gun', align: 'center' }
])
function onReset () {
store.resetFilters()
store.fetchBalances()
}
function onToggle12Changed (val) {
if (val) {
store.filters.excludeZeroBalance13 = false
}
}
function onToggle13Changed (val) {
if (val) {
store.filters.excludeZeroBalance12 = false
}
}
function toggleFiltersCollapsed () {
filtersCollapsed.value = !filtersCollapsed.value
}
function toggleGroup (key) {
expanded.value[key] = !expanded.value[key]
if (!expanded.value[key]) {
allDetailsOpen.value = false
return
}
allDetailsOpen.value =
store.summaryRows.length > 0 &&
store.summaryRows.every(r => expanded.value[r.group_key])
}
function toggleAllDetails () {
allDetailsOpen.value = !allDetailsOpen.value
if (allDetailsOpen.value) {
const next = {}
for (const row of store.summaryRows) {
next[row.group_key] = true
}
expanded.value = next
return
}
expanded.value = {}
}
async function downloadAgingBalancePDF (detailed) {
if (!canExportFinance.value) {
$q.notify({ type: 'negative', message: 'PDF export yetkiniz yok', position: 'top-right' })
return
}
if (!store.hasFetched) {
$q.notify({ type: 'warning', message: 'Önce Bakiyeleri Getir ile veri yükleyin.', position: 'top-right' })
return
}
try {
const params = {
cari_search: String(store.filters.cariSearch || '').trim(),
cari_ilk_grup: (store.filters.cariIlkGrup || []).join(','),
piyasa: (store.filters.piyasa || []).join(','),
temsilci: (store.filters.temsilci || []).join(','),
risk_durumu: (store.filters.riskDurumu || []).join(','),
islem_tipi: (store.filters.islemTipi || []).join(','),
ulke: (store.filters.ulke || []).join(','),
il: (store.filters.il || []).join(','),
ilce: (store.filters.ilce || []).join(','),
exclude_zero_12: store.filters.excludeZeroBalance12 ? '1' : '0',
exclude_zero_13: store.filters.excludeZeroBalance13 ? '1' : '0',
detailed: detailed ? '1' : '0'
}
const blob = await download('/finance/account-aging-statement/export-pdf', params)
const pdfUrl = window.URL.createObjectURL(new Blob([blob], { type: 'application/pdf' }))
window.open(pdfUrl, '_blank')
} catch (err) {
const detail = await extractApiErrorDetail(err?.original || err)
$q.notify({
type: 'negative',
message: detail || 'PDF oluşturulamadı',
position: 'top-right'
})
}
}
async function downloadAgingBalanceExcel () {
if (!canExportFinance.value) {
$q.notify({ type: 'negative', message: 'Excel export yetkiniz yok', position: 'top-right' })
return
}
if (!store.hasFetched) {
$q.notify({ type: 'warning', message: 'Önce Bakiyeleri Getir ile veri yükleyin.', position: 'top-right' })
return
}
try {
const params = {
cari_search: String(store.filters.cariSearch || '').trim(),
cari_ilk_grup: (store.filters.cariIlkGrup || []).join(','),
piyasa: (store.filters.piyasa || []).join(','),
temsilci: (store.filters.temsilci || []).join(','),
risk_durumu: (store.filters.riskDurumu || []).join(','),
islem_tipi: (store.filters.islemTipi || []).join(','),
ulke: (store.filters.ulke || []).join(','),
il: (store.filters.il || []).join(','),
ilce: (store.filters.ilce || []).join(','),
exclude_zero_12: store.filters.excludeZeroBalance12 ? '1' : '0',
exclude_zero_13: store.filters.excludeZeroBalance13 ? '1' : '0'
}
const file = await download('/finance/account-aging-statement/export-excel', params)
const blob = new Blob(
[file],
{ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }
)
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'cari_yaslandirmali_bakiye_listesi.xlsx'
document.body.appendChild(a)
a.click()
a.remove()
window.URL.revokeObjectURL(url)
} catch (err) {
const detail = await extractApiErrorDetail(err?.original || err)
$q.notify({
type: 'negative',
message: detail || 'Excel oluşturulamadı',
position: 'top-right'
})
}
}
function formatAmount (value) {
const n = Number(value || 0)
return new Intl.NumberFormat('tr-TR', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(n)
}
function formatDay (value) {
const n = Number(value || 0)
return new Intl.NumberFormat('tr-TR', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(n)
}
function selectionLabel (arr, label) {
const count = Array.isArray(arr) ? arr.length : 0
if (count === 0) return `Tümü (${label})`
if (count === 1) return '1 seçim'
return `${count} seçim`
}
function totalCellValue (colName) {
if (colName === 'expand') return 'Toplam'
if (colName === 'piyasa') return '-'
if (colName === 'temsilci') return '-'
if (colName === 'risk_durumu') return '-'
if (colName === 'prbr_1_2') return formatCurrencyMap(totalByCurrency('1_2'))
if (colName === 'prbr_1_3') return formatCurrencyMap(totalByCurrency('1_3'))
if (colName === 'usd_bakiye_1_2') return formatAmount(liveTotals.value.usd_bakiye_1_2)
if (colName === 'tl_bakiye_1_2') return formatAmount(liveTotals.value.tl_bakiye_1_2)
if (colName === 'usd_bakiye_1_3') return formatAmount(liveTotals.value.usd_bakiye_1_3)
if (colName === 'tl_bakiye_1_3') return formatAmount(liveTotals.value.tl_bakiye_1_3)
if (colName === 'vade_gun') return formatDay(liveTotals.value.vade_count > 0 ? liveTotals.value.vade_gun_sum / liveTotals.value.vade_count : 0)
if (colName === 'vade_belge_tarihi_gun') return formatDay(liveTotals.value.vade_count > 0 ? liveTotals.value.vade_belge_sum / liveTotals.value.vade_count : 0)
return '-'
}
function totalByCurrency (tip) {
const key = tip === '1_2' ? 'bakiye_1_2_map' : 'bakiye_1_3_map'
const out = {}
for (const r of store.summaryRows) {
const m = r[key] || {}
for (const [curr, val] of Object.entries(m)) {
out[curr] = (Number(out[curr]) || 0) + (Number(val) || 0)
}
}
return out
}
function formatCurrencyMap (mapObj) {
const entries = Object.entries(mapObj || {})
.filter(([, amount]) => Number(amount) !== 0)
.sort((a, b) => a[0].localeCompare(b[0], 'en'))
if (!entries.length) return '-'
return entries
.map(([curr, amount]) => `${curr}: ${formatAmount(amount)}`)
.join('\n')
}
function formatRowPrBr (row, tip) {
const curr = String(row.cari_doviz || '').trim().toUpperCase() || 'N/A'
const amount = tip === '1_2'
? (Number(row.bakiye_1_2) || 0)
: (Number(row.bakiye_1_3) || 0)
if (amount === 0) return '-'
return `${curr} ${formatAmount(amount)}`
}
onMounted(async () => {
store.filters.selectedDate = new Date().toISOString().slice(0, 10)
await store.fetchBalances()
})
</script>
<style scoped>
.page-layout {
height: calc(100vh - 110px);
display: flex;
flex-direction: column;
overflow: hidden;
}
.filter-sticky {
position: sticky;
top: 0;
z-index: 20;
background: #fff;
padding-bottom: 6px;
}
.filter-sticky.collapsed {
padding-bottom: 0;
}
.top-actions.single-line {
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
padding-bottom: 4px;
}
.top-actions.single-line > [class*='col-'],
.top-actions.single-line > .col-auto {
flex: 0 0 auto;
min-width: 220px;
}
.top-actions.single-line > .col-auto {
min-width: auto;
}
.filters-panel {
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 8px;
background: #fafafa;
}
.compact-select :deep(.q-field__control) {
min-height: 40px;
}
.compact-select :deep(.q-field__native),
.compact-select :deep(.q-field__input) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.table-area {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.sticky-bar {
position: sticky;
top: 0;
z-index: 9;
}
.balance-table {
flex: 1;
min-height: 0;
}
.balance-table :deep(.q-table__container) {
height: 100%;
display: flex;
flex-direction: column;
}
.balance-table :deep(.q-table__top) {
position: sticky;
top: 0;
z-index: 6;
background: #fff;
}
.balance-table :deep(.q-table__middle) {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
}
.balance-table :deep(.header-row th) {
position: sticky;
top: 0;
z-index: 8;
background: var(--q-primary);
color: #fff;
font-weight: 600;
font-family: 'Roboto', sans-serif;
}
.balance-table :deep(.totals-row th) {
position: sticky;
top: 38px;
z-index: 7;
background: var(--q-secondary);
color: var(--q-dark);
font-weight: 700;
font-family: 'Roboto', sans-serif;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.detail-table :deep(.q-table__middle) {
max-height: 320px;
}
.balance-table :deep(.sub-header-row td) {
background: #fff;
border-bottom: 2px solid rgba(0, 0, 0, 0.18);
font-weight: 600;
}
.balance-table :deep(.sub-header-row td:first-child) {
border-left: 3px solid var(--q-primary);
}
.balance-table :deep(.detail-host-row td) {
background: #f7f7f7;
border-bottom: 10px solid #fff;
padding-top: 10px;
padding-bottom: 12px;
}
.detail-wrap {
border: 1px solid rgba(0, 0, 0, 0.14);
border-left: 4px solid var(--q-secondary);
border-radius: 6px;
background: #fff;
padding: 6px;
}
.balance-table :deep(.header-row th) {
white-space: pre-line;
line-height: 1.15;
}
.prbr-cell {
white-space: pre-line;
word-break: break-word;
line-height: 1.25;
}
.balance-table :deep(th),
.balance-table :deep(td),
.detail-table :deep(th),
.detail-table :deep(td) {
white-space: normal !important;
word-break: break-word;
overflow-wrap: anywhere;
font-size: 11px;
line-height: 1.2;
padding: 4px 6px !important;
}
.balance-table :deep(.q-table__table),
.detail-table :deep(.q-table__table) {
width: 100% !important;
}
.detail-table :deep(.q-table__middle) {
overflow-x: hidden;
}
</style>

View File

@@ -84,6 +84,14 @@
dense dense
/> />
</div> </div>
<div class="col-12 col-sm-6 col-md-2">
<q-toggle
v-model="excludeOpening"
label="Devir bakiyesiz listele"
color="primary"
dense
/>
</div>
<div class="col-auto"> <div class="col-auto">
<q-btn color="primary" icon="filter_alt" label="Filtrele" @click="onFilterClick" /> <q-btn color="primary" icon="filter_alt" label="Filtrele" @click="onFilterClick" />
</div> </div>
@@ -358,6 +366,7 @@ const monetaryTypeOptions = [
{ label: '1-3 r hesap', value: ['1', '3'] } { label: '1-3 r hesap', value: ['1', '3'] }
] ]
const selectedMonType = ref(monetaryTypeOptions[0].value) const selectedMonType = ref(monetaryTypeOptions[0].value)
const excludeOpening = ref(false)
/* Expand kontrolü */ /* Expand kontrolü */
const expandedRows = ref({}) const expandedRows = ref({})
@@ -404,7 +413,8 @@ async function onFilterClick() {
enddate: dateTo.value, enddate: dateTo.value,
accountcode: selectedCari.value, accountcode: selectedCari.value,
langcode: 'TR', langcode: 'TR',
parislemler: selectedMonType.value parislemler: selectedMonType.value,
excludeopening: excludeOpening.value
}) })
await detailStore.loadDetails({ await detailStore.loadDetails({
@@ -454,6 +464,7 @@ function resetFilters() {
dateFrom.value = '' dateFrom.value = ''
dateTo.value = '' dateTo.value = ''
selectedMonType.value = monetaryTypeOptions[0].value selectedMonType.value = monetaryTypeOptions[0].value
excludeOpening.value = false
statementheaderStore.headers = [] statementheaderStore.headers = []
detailStore.reset() detailStore.reset()
} }
@@ -715,4 +726,3 @@ async function CurrheadDownload() {
} }
} }
</style> </style>

View File

@@ -149,7 +149,7 @@ const routes = [
{ {
path: 'aged-customer-balance-list', path: 'aged-customer-balance-list',
name: 'aged-customer-balance-list', name: 'aged-customer-balance-list',
redirect: { name: 'account-aging-statement' }, component: () => import('pages/AgingCustomerBalancelist.go.vue'),
meta: { permission: 'finance:view' } meta: { permission: 'finance:view' }
}, },

View File

@@ -180,7 +180,7 @@ export const useAccountAgingBalanceStore = defineStore('accountAgingBalance', {
try { try {
this.filters.selectedDate = new Date().toISOString().slice(0, 10) 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: { params: {
cari_search: String(this.filters.cariSearch || '').trim(), cari_search: String(this.filters.cariSearch || '').trim(),
cari_ilk_grup: (this.filters.cariIlkGrup || []).join(','), cari_ilk_grup: (this.filters.cariIlkGrup || []).join(','),

View File

@@ -50,32 +50,25 @@ export const useStatementAgingStore = defineStore('statementAging', {
const currencyKey = `${cari8}|${curr}` const currencyKey = `${cari8}|${curr}`
const tutar = Number(row?.eslesen_tutar) || 0 const tutar = Number(row?.eslesen_tutar) || 0
const usd = Number(row?.usd_tutar) || 0 const usd = Number(row?.usd_tutar) || 0
const trY = Number(row?.try_tutar) || 0 const absUsd = Math.abs(usd)
const absTry = Math.abs(trY)
const gun = Number(row?.gun_sayisi) || 0 const gun = Number(row?.gun_sayisi) || 0
const gunDoc = Number(row?.gun_sayisi_docdate) || 0 const gunDoc = Number(row?.gun_sayisi_docdate) || 0
const aciklama = String(row?.aciklama || '').toUpperCase() const aciklama = String(row?.aciklama || '').toUpperCase()
const isAcik = aciklama === 'ACIKKALEM'
if (!masterMap[masterKey]) { if (!masterMap[masterKey]) {
masterMap[masterKey] = { masterMap[masterKey] = {
group_key: masterKey, group_key: masterKey,
cari8: masterKey, cari8: masterKey,
cari_detay: String(row?.cari_detay || '').trim(), cari_detay: String(row?.cari_detay || '').trim(),
acik_kalem_tutari_usd: 0, satir_sayisi: 0,
acik_kalem_tutari_try: 0, toplam_usd: 0,
acik_kalem_ort_vade_gun: 0, normal_usd: 0,
acik_kalem_ort_belge_gun: 0, acik_kalem_usd: 0,
normal_usd_tutar: 0, weighted_gun_sum: 0,
normal_try_tutar: 0, weighted_gun_doc_sum: 0,
ortalama_vade_gun: 0, weighted_base: 0,
ortalama_belge_gun: 0, ortalama_gun: 0,
weighted_all_base: 0, ortalama_gun_docdate: 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,
} }
} }
@@ -85,46 +78,48 @@ export const useStatementAgingStore = defineStore('statementAging', {
master_key: masterKey, master_key: masterKey,
cari8, cari8,
cari_detay: String(row?.cari_detay || '').trim(), cari_detay: String(row?.cari_detay || '').trim(),
doviz_cinsi: curr, doc_currency_code: curr,
acik_kalem_tutari: 0, satir_sayisi: 0,
acik_kalem_usd: 0, toplam_tutar: 0,
acik_kalem_try: 0, toplam_usd: 0,
ort_gun: 0, normal_tutar: 0,
ort_belge_gun: 0, acik_kalem_tutar: 0,
weighted_open_base: 0, weighted_gun_sum: 0,
weighted_open_gun_sum: 0, weighted_gun_doc_sum: 0,
weighted_open_doc_sum: 0 weighted_base: 0,
ortalama_gun: 0,
ortalama_gun_docdate: 0
} }
} }
const m = masterMap[masterKey] const m = masterMap[masterKey]
const c = currencyMap[currencyKey] const c = currencyMap[currencyKey]
if (isAcik) { m.satir_sayisi += 1
m.acik_kalem_tutari_usd += usd m.toplam_usd += usd
m.acik_kalem_tutari_try += trY if (aciklama === 'ACIKKALEM') {
c.acik_kalem_tutari += tutar m.acik_kalem_usd += usd
c.acik_kalem_usd += usd
c.acik_kalem_try += trY
} else { } else {
m.normal_usd_tutar += usd m.normal_usd += usd
m.normal_try_tutar += trY
} }
if (absTry > 0) { c.satir_sayisi += 1
m.weighted_all_base += absTry c.toplam_tutar += tutar
m.weighted_all_gun_sum += absTry * gun c.toplam_usd += usd
m.weighted_all_doc_sum += absTry * gunDoc if (aciklama === 'ACIKKALEM') {
c.acik_kalem_tutar += tutar
if (isAcik) { } else {
m.weighted_open_base += absTry c.normal_tutar += tutar
m.weighted_open_gun_sum += absTry * gun
m.weighted_open_doc_sum += absTry * gunDoc
c.weighted_open_base += absTry
c.weighted_open_gun_sum += absTry * gun
c.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_base += absUsd
c.weighted_gun_sum += absUsd * gun
c.weighted_gun_doc_sum += absUsd * gunDoc
} }
if (!detailMap[currencyKey]) detailMap[currencyKey] = [] if (!detailMap[currencyKey]) detailMap[currencyKey] = []
@@ -134,10 +129,8 @@ export const useStatementAgingStore = defineStore('statementAging', {
this.masterRows = Object.values(masterMap) this.masterRows = Object.values(masterMap)
.map((m) => ({ .map((m) => ({
...m, ...m,
acik_kalem_ort_vade_gun: m.weighted_open_base > 0 ? ceilDay(m.weighted_open_gun_sum / m.weighted_open_base) : 0, ortalama_gun: m.weighted_base > 0 ? (m.weighted_gun_sum / m.weighted_base) : 0,
acik_kalem_ort_belge_gun: m.weighted_open_base > 0 ? ceilDay(m.weighted_open_doc_sum / m.weighted_open_base) : 0, ortalama_gun_docdate: m.weighted_base > 0 ? (m.weighted_gun_doc_sum / m.weighted_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
})) }))
.sort((a, b) => String(a.cari8).localeCompare(String(b.cari8), 'tr', { sensitivity: 'base' })) .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)) { for (const c of Object.values(currencyMap)) {
const row = { const row = {
...c, ...c,
ort_gun: c.weighted_open_base > 0 ? ceilDay(c.weighted_open_gun_sum / c.weighted_open_base) : 0, ortalama_gun: c.weighted_base > 0 ? (c.weighted_gun_sum / c.weighted_base) : 0,
ort_belge_gun: c.weighted_open_base > 0 ? ceilDay(c.weighted_open_doc_sum / c.weighted_open_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] = [] if (!currencyByMaster[row.master_key]) currencyByMaster[row.master_key] = []
currencyByMaster[row.master_key].push(row) currencyByMaster[row.master_key].push(row)
} }
for (const key of Object.keys(currencyByMaster)) { 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 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 this.detailByCurrency = detailMap
}, },
@@ -206,17 +185,9 @@ function normalizeRowKeys(row) {
odeme_doc_date: row.OdemeDocDate ?? row.odeme_doc_date ?? null, odeme_doc_date: row.OdemeDocDate ?? row.odeme_doc_date ?? null,
eslesen_tutar: Number(row.EslesenTutar ?? row.eslesen_tutar ?? 0), eslesen_tutar: Number(row.EslesenTutar ?? row.eslesen_tutar ?? 0),
usd_tutar: Number(row.UsdTutar ?? row.usd_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: Number(row.GunSayisi ?? row.gun_sayisi ?? 0),
gun_sayisi_docdate: Number(row.GunSayisi_DocDate ?? row.gun_sayisi_docdate ?? 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, aciklama: row.Aciklama ?? row.aciklama ?? null,
doc_currency_code: row.DocCurrencyCode ?? row.doc_currency_code ?? 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)
}