diff --git a/svc/.env.local b/svc/.env.local index 8a42090..aa2857f 100644 --- a/svc/.env.local +++ b/svc/.env.local @@ -22,7 +22,7 @@ UI_DIR=/opt/bssapp/ui/dist # DATABASES # =============================== POSTGRES_CONN=host=46.224.33.150 port=5432 user=postgres password=tayitkan dbname=baggib2b sslmode=disable -MSSQL_CONN=sqlserver://sa:Gil_0150@100.127.186.137:1433?database=BAGGI_V3&encrypt=disable +MSSQL_CONN=sqlserver://sa:Gil_0150@10.0.0.9:1433?database=BAGGI_V3&encrypt=disable # =============================== # PDF diff --git a/svc/db/mssql.go b/svc/db/mssql.go index 2f30681..f14fe26 100644 --- a/svc/db/mssql.go +++ b/svc/db/mssql.go @@ -3,7 +3,6 @@ package db import ( "database/sql" "fmt" - "log" "os" "strings" @@ -13,23 +12,24 @@ import ( var MssqlDB *sql.DB // ConnectMSSQL MSSQL baglantisini ortam degiskeninden baslatir. -func ConnectMSSQL() { +func ConnectMSSQL() error { connString := strings.TrimSpace(os.Getenv("MSSQL_CONN")) if connString == "" { - log.Fatal("MSSQL_CONN tanımlı değil") + return fmt.Errorf("MSSQL_CONN tanımlı değil") } var err error MssqlDB, err = sql.Open("sqlserver", connString) if err != nil { - log.Fatal("MSSQL bağlantı hatası:", err) + return fmt.Errorf("MSSQL bağlantı hatası: %w", err) } if err = MssqlDB.Ping(); err != nil { - log.Fatal("MSSQL erişilemiyor:", err) + return fmt.Errorf("MSSQL erişilemiyor: %w", err) } fmt.Println("MSSQL bağlantısı başarılı") + return nil } func GetDB() *sql.DB { diff --git a/svc/main.go b/svc/main.go index d754445..1c9f3b7 100644 --- a/svc/main.go +++ b/svc/main.go @@ -425,6 +425,12 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router wrapV3(routes.ExportStatementHeaderReportPDFHandler(mssql)), ) + bindV3(r, pgDB, + "/api/finance/customer-balances", "GET", + "finance", "view", + wrapV3(http.HandlerFunc(routes.GetCustomerBalanceListHandler)), + ) + // ============================================================ // REPORT (STATEMENTS) // ============================================================ @@ -576,6 +582,7 @@ func main() { // ------------------------------------------------------- // 🔑 ENV // ------------------------------------------------------- + // Önce .env + mail.env yükle. MSSQL başarısızsa .env.local dene. if err := godotenv.Load(".env", "mail.env"); err != nil { log.Println("⚠️ .env / mail.env bulunamadı") } @@ -589,7 +596,15 @@ func main() { // ------------------------------------------------------- // 🔗 DATABASE // ------------------------------------------------------- - db.ConnectMSSQL() + if err := db.ConnectMSSQL(); err != nil { + log.Println("⚠️ MSSQL ilk deneme başarısız:", err) + if err2 := godotenv.Overload(".env.local"); err2 != nil { + log.Println("⚠️ .env.local bulunamadı") + } + if err3 := db.ConnectMSSQL(); err3 != nil { + log.Fatal(err3) + } + } pgDB, err := db.ConnectPostgres() if err != nil { diff --git a/svc/models/customer_balance_list.go b/svc/models/customer_balance_list.go new file mode 100644 index 0000000..75e67d0 --- /dev/null +++ b/svc/models/customer_balance_list.go @@ -0,0 +1,36 @@ +package models + +type CustomerBalanceListParams struct { + SelectedDate string + CariSearch string + CariIlkGrup string + Piyasa string + Temsilci string + RiskDurumu string + IslemTipi string + Ulke string +} + +type CustomerBalanceListRow struct { + CariIlkGrup string `json:"cari_ilk_grup"` + Piyasa string `json:"piyasa"` + Temsilci string `json:"temsilci"` + Sirket string `json:"sirket"` + AnaCariKodu string `json:"ana_cari_kodu"` + AnaCariAdi string `json:"ana_cari_adi"` + CariKodu string `json:"cari_kodu"` + CariDetay string `json:"cari_detay"` + Ozellik03 string `json:"ozellik03"` + Ozellik05 string `json:"ozellik05"` + Ozellik06 string `json:"ozellik06"` + Ozellik07 string `json:"ozellik07"` + CariDoviz string `json:"cari_doviz"` + Bakiye12 float64 `json:"bakiye_1_2"` + TLBakiye12 float64 `json:"tl_bakiye_1_2"` + USDBakiye12 float64 `json:"usd_bakiye_1_2"` + Bakiye13 float64 `json:"bakiye_1_3"` + TLBakiye13 float64 `json:"tl_bakiye_1_3"` + USDBakiye13 float64 `json:"usd_bakiye_1_3"` + HesapAlinmayanGun NullInt32 `json:"hesap_alinmayan_gun"` + KalanFaturaOrtalamaVadeTarihi NullString `json:"kalan_fatura_ortalama_vade_tarihi"` +} diff --git a/svc/queries/customer_balance_list.go b/svc/queries/customer_balance_list.go new file mode 100644 index 0000000..a6d1539 --- /dev/null +++ b/svc/queries/customer_balance_list.go @@ -0,0 +1,200 @@ +package queries + +import ( + "bssapp-backend/db" + "bssapp-backend/internal/authz" + "bssapp-backend/models" + "context" + "database/sql" + "fmt" + "strings" +) + +func GetCustomerBalanceList(ctx context.Context, params models.CustomerBalanceListParams) ([]models.CustomerBalanceListRow, error) { + // AuthZ bazli piyasa filtresi + piyasaFilter := authz.BuildMSSQLPiyasaFilter(ctx, "D.Ozellik01") + if strings.TrimSpace(piyasaFilter) == "" { + piyasaFilter = "1=1" + } + + // Dinamik WHERE insa et + where := make([]string, 0, 16) + where = append(where, "D.Islem_Tarihi < DATEADD(DAY, 1, @SecilenTarih)") + where = append(where, piyasaFilter) + + if params.CariIlkGrup != "" { + where = append(where, "D.Ozellik08 = @CariIlkGrup") + } + if params.Piyasa != "" { + where = append(where, "D.Ozellik01 = @Piyasa") + } + if params.Temsilci != "" { + where = append(where, "COALESCE(NULLIF(D.Ozellik02, ''), D.Ozellik09) = @Temsilci") + } + if params.RiskDurumu != "" { + where = append(where, "D.Ozellik03 = @RiskDurumu") + } + if params.IslemTipi != "" { + where = append(where, "D.PislemTipi = @IslemTipi") + } + if params.Ulke != "" { + where = append(where, "D.Ozellik05 = @Ulke") + } + whereSQL := strings.Join(where, "\n AND ") + cariSearchLike := "%" + strings.TrimSpace(params.CariSearch) + "%" + outerWhere := "1=1" + if strings.TrimSpace(params.CariSearch) != "" { + outerWhere = `(LEFT(B.CariKodu, 8) COLLATE Turkish_100_CI_AI LIKE @CariSearchLike + OR B.CariKodu COLLATE Turkish_100_CI_AI LIKE @CariSearchLike + OR B.CariDetay COLLATE Turkish_100_CI_AI LIKE @CariSearchLike + OR AC.ANA_CARI_ADI COLLATE Turkish_100_CI_AI LIKE @CariSearchLike)` + } + + const queryTemplate = ` +;WITH CTE_ANA_CARI AS ( + SELECT + ANA_CARI_KODU = LEFT(CariKodu, 8), + CariDetay, + rn = ROW_NUMBER() OVER ( + PARTITION BY LEFT(CariKodu, 8) + ORDER BY CariKodu + ) + FROM dbo.MK_CARI_ILETISIM WITH (NOLOCK) +), +ANA_CARI AS ( + SELECT + ANA_CARI_KODU, + CariDetay AS ANA_CARI_ADI + FROM CTE_ANA_CARI + WHERE rn = 1 +), +BASE AS ( + SELECT + D.SirketKodu, + D.SirketDetay, + D.CariKodu, + D.CariDetay, + D.CariDoviz, + D.Ozellik01, + D.Ozellik02, + D.Ozellik03, + D.Ozellik05, + D.Ozellik06, + D.Ozellik07, + D.Ozellik08, + D.Ozellik09, + D.PislemTipi, + D.Bakiye, + D.KurBakiye, + D.Son_Guncel_Kur + FROM dbo.DENEME02DENEME AS D WITH (NOLOCK) + WHERE %s +) +SELECT + B.Ozellik08 AS CARI_ILK_GRUP, + B.Ozellik01 AS PIYASA, + COALESCE(NULLIF(B.Ozellik02, ''), B.Ozellik09) AS Temsilci, + LEFT(B.SirketDetay, 10) AS Sirket, + LEFT(B.CariKodu, 8) AS ANA_CARI_KODU, + AC.ANA_CARI_ADI, + B.CariKodu, + B.CariDetay, + B.Ozellik03, + B.Ozellik05, + B.Ozellik06, + B.Ozellik07, + B.CariDoviz, + ISNULL(SUM(CASE WHEN B.PislemTipi = '1_2' THEN ISNULL(B.Bakiye, 0) ELSE 0 END), 0) AS Bakiye_1_2, + ISNULL(SUM(CASE WHEN B.PislemTipi = '1_2' THEN ISNULL(B.KurBakiye, 0) ELSE 0 END), 0) AS TL_Bakiye_1_2, + ISNULL( + SUM(CASE WHEN B.PislemTipi = '1_2' THEN ISNULL(B.KurBakiye, 0) ELSE 0 END) / NULLIF(MIN(B.Son_Guncel_Kur), 0), + 0 + ) AS USD_Bakiye_1_2, + ISNULL(SUM(CASE WHEN B.PislemTipi = '1_3' THEN ISNULL(B.Bakiye, 0) ELSE 0 END), 0) AS Bakiye_1_3, + ISNULL(SUM(CASE WHEN B.PislemTipi = '1_3' THEN ISNULL(B.KurBakiye, 0) ELSE 0 END), 0) AS TL_Bakiye_1_3, + ISNULL( + SUM(CASE WHEN B.PislemTipi = '1_3' THEN ISNULL(B.KurBakiye, 0) ELSE 0 END) / NULLIF(MIN(B.Son_Guncel_Kur), 0), + 0 + ) AS USD_Bakiye_1_3, + CAST(NULL AS int) AS Hesap_Alinmayan_Gun, + CAST(NULL AS varchar(32)) AS Kalan_Fatura_Ortalama_Vade_Tarihi +FROM BASE AS B +LEFT JOIN ANA_CARI AS AC + ON AC.ANA_CARI_KODU = LEFT(B.CariKodu, 8) +WHERE %s +GROUP BY + B.Ozellik08, + B.Ozellik01, + COALESCE(NULLIF(B.Ozellik02, ''), B.Ozellik09), + LEFT(B.SirketDetay, 10), + LEFT(B.CariKodu, 8), + AC.ANA_CARI_ADI, + B.CariKodu, + B.CariDetay, + B.Ozellik03, + B.Ozellik05, + B.Ozellik06, + B.Ozellik07, + B.CariDoviz +ORDER BY + LEFT(B.SirketDetay, 10), + B.CariKodu +OPTION (RECOMPILE); +` + query := fmt.Sprintf(queryTemplate, whereSQL, outerWhere) + + rows, err := db.MssqlDB.QueryContext( + ctx, + query, + sql.Named("SecilenTarih", params.SelectedDate), + sql.Named("CariIlkGrup", params.CariIlkGrup), + sql.Named("Piyasa", params.Piyasa), + sql.Named("Temsilci", params.Temsilci), + sql.Named("RiskDurumu", params.RiskDurumu), + sql.Named("IslemTipi", params.IslemTipi), + sql.Named("Ulke", params.Ulke), + sql.Named("CariSearchLike", cariSearchLike), + ) + if err != nil { + return nil, fmt.Errorf("MSSQL query error: %w", err) + } + defer rows.Close() + + out := make([]models.CustomerBalanceListRow, 0, 512) + + for rows.Next() { + var r models.CustomerBalanceListRow + if err := rows.Scan( + &r.CariIlkGrup, + &r.Piyasa, + &r.Temsilci, + &r.Sirket, + &r.AnaCariKodu, + &r.AnaCariAdi, + &r.CariKodu, + &r.CariDetay, + &r.Ozellik03, + &r.Ozellik05, + &r.Ozellik06, + &r.Ozellik07, + &r.CariDoviz, + &r.Bakiye12, + &r.TLBakiye12, + &r.USDBakiye12, + &r.Bakiye13, + &r.TLBakiye13, + &r.USDBakiye13, + &r.HesapAlinmayanGun, + &r.KalanFaturaOrtalamaVadeTarihi, + ); err != nil { + return nil, fmt.Errorf("row scan error: %w", err) + } + out = append(out, r) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows iteration error: %w", err) + } + + return out, nil +} diff --git a/svc/routes/customer_balance_list.go b/svc/routes/customer_balance_list.go new file mode 100644 index 0000000..42db345 --- /dev/null +++ b/svc/routes/customer_balance_list.go @@ -0,0 +1,49 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/models" + "bssapp-backend/queries" + "encoding/json" + "log" + "net/http" + "strings" + "time" +) + +// GET /api/finance/customer-balances +func GetCustomerBalanceListHandler(w http.ResponseWriter, r *http.Request) { + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + selectedDate := strings.TrimSpace(r.URL.Query().Get("selected_date")) + if selectedDate == "" { + selectedDate = time.Now().Format("2006-01-02") + } + + params := models.CustomerBalanceListParams{ + SelectedDate: selectedDate, + CariSearch: strings.TrimSpace(r.URL.Query().Get("cari_search")), + CariIlkGrup: strings.TrimSpace(r.URL.Query().Get("cari_ilk_grup")), + Piyasa: strings.TrimSpace(r.URL.Query().Get("piyasa")), + Temsilci: strings.TrimSpace(r.URL.Query().Get("temsilci")), + RiskDurumu: strings.TrimSpace(r.URL.Query().Get("risk_durumu")), + IslemTipi: strings.TrimSpace(r.URL.Query().Get("islem_tipi")), + Ulke: strings.TrimSpace(r.URL.Query().Get("ulke")), + } + + rows, err := queries.GetCustomerBalanceList(r.Context(), params) + if err != nil { + log.Println("GetCustomerBalanceList error:", err) + http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if err := json.NewEncoder(w).Encode(rows); err != nil { + log.Println("GetCustomerBalanceList json encode error:", err) + } +} diff --git a/svc/run.log b/svc/run.log new file mode 100644 index 0000000..9178445 --- /dev/null +++ b/svc/run.log @@ -0,0 +1,137 @@ +2026/02/23 12:29:31 🔥🔥🔥 BSSAPP BACKEND STARTED — LOGIN ROUTE SHOULD EXIST 🔥🔥🔥 +2026/02/23 12:29:31 🔐 JWT_SECRET yüklendi +MSSQL bağlantısı başarılı +2026/02/23 12:29:31 PostgreSQL bağlantısı başarılı +2026/02/23 12:29:31 ✅ Admin dept permissions seeded +2026/02/23 12:29:31 🟢 auditlog Init called, buffer: 1000 +2026/02/23 12:29:31 🕵️ AuditLog sistemi başlatıldı (buffer=1000) +2026/02/23 12:29:31 ✉️ Graph Mailer hazır (App-only token) | from=baggiss@baggi.com.tr +2026/02/23 12:29:31 ✉️ Graph Mailer hazır +2026/02/23 12:29:31 🟢 auditlog worker STARTED +📋 [DEBUG] İlk 10 kullanıcı: + - 1 : ctengiz + - 2 : ali.kale + - 5 : mehmet.keçeci + - 6 : mert.keçeci + - 7 : samet.keçeci + - 9 : orhan.caliskan + - 10 : nilgun.sara + - 14 : rustem.kurbanov + - 15 : caner.akyol + - 16 : kemal.matyakupov +2026/02/23 12:29:32 ✅ Route+Perm registered → POST /api/auth/login [auth:login] +2026/02/23 12:29:32 ✅ Route+Perm registered → POST /api/auth/refresh [auth:refresh] +2026/02/23 12:29:32 ✅ Route+Perm registered → POST /api/password/forgot [auth:update] +2026/02/23 12:29:33 ✅ Route+Perm registered → GET /api/password/reset/validate/{token} [auth:view] +2026/02/23 12:29:33 ✅ Route+Perm registered → POST /api/password/reset [auth:update] +2026/02/23 12:29:34 ✅ Route+Perm registered → POST /api/password/change [auth:update] +2026/02/23 12:29:34 ✅ Route+Perm registered → GET /api/activity-logs [system:read] +2026/02/23 12:29:35 ✅ Route+Perm registered → POST /api/test-mail [system:update] +2026/02/23 12:29:35 ✅ Route+Perm registered → GET /api/roles/{id}/permissions [system:update] +2026/02/23 12:29:36 ✅ Route+Perm registered → POST /api/roles/{id}/permissions [system:update] +2026/02/23 12:29:36 ✅ Route+Perm registered → GET /api/users/{id}/permissions [system:update] +2026/02/23 12:29:36 ✅ Route+Perm registered → POST /api/users/{id}/permissions [system:update] +2026/02/23 12:29:37 ✅ Route+Perm registered → GET /api/permissions/routes [system:view] +2026/02/23 12:29:37 ✅ Route+Perm registered → GET /api/permissions/effective [system:view] +2026/02/23 12:29:38 ✅ Route+Perm registered → GET /api/permissions/matrix [system:view] +2026/02/23 12:29:38 ✅ Route+Perm registered → GET /api/role-dept-permissions/list [system:update] +2026/02/23 12:29:38 ✅ Route+Perm registered → GET /api/roles/{roleId}/departments/{deptCode}/permissions [system:update] +2026/02/23 12:29:39 ✅ Route+Perm registered → POST /api/roles/{roleId}/departments/{deptCode}/permissions [system:update] +2026/02/23 12:29:39 ✅ Route+Perm registered → GET /api/users/list [user:view] +2026/02/23 12:29:40 ✅ Route+Perm registered → POST /api/users [user:insert] +2026/02/23 12:29:40 ✅ Route+Perm registered → GET /api/users/{id} [user:update] +2026/02/23 12:29:41 ✅ Route+Perm registered → PUT /api/users/{id} [user:update] +2026/02/23 12:29:41 ✅ Route+Perm registered → DELETE /api/users/{id} [user:delete] +2026/02/23 12:29:41 ✅ Route+Perm registered → POST /api/users/{id}/admin-reset-password [user:update] +2026/02/23 12:29:42 ✅ Route+Perm registered → POST /api/users/{id}/send-password-mail [user:update] +2026/02/23 12:29:42 ✅ Route+Perm registered → POST /api/users/create [user:insert] +2026/02/23 12:29:43 ✅ Route+Perm registered → GET /api/lookups/users-perm [user:view] +2026/02/23 12:29:43 ✅ Route+Perm registered → GET /api/lookups/roles-perm [user:view] +2026/02/23 12:29:43 ✅ Route+Perm registered → GET /api/lookups/departments-perm [user:view] +2026/02/23 12:29:44 ✅ Route+Perm registered → GET /api/lookups/modules [user:view] +2026/02/23 12:29:44 ✅ Route+Perm registered → GET /api/lookups/roles [user:view] +2026/02/23 12:29:45 ✅ Route+Perm registered → GET /api/lookups/departments [user:view] +2026/02/23 12:29:45 ✅ Route+Perm registered → GET /api/lookups/nebim-users [user:view] +2026/02/23 12:29:46 ✅ Route+Perm registered → GET /api/lookups/piyasalar [user:view] +2026/02/23 12:29:46 ✅ Route+Perm registered → GET /api/accounts [customer:view] +2026/02/23 12:29:46 ✅ Route+Perm registered → GET /api/customer-list [customer:view] +2026/02/23 12:29:47 ✅ Route+Perm registered → GET /api/today-currency [finance:view] +2026/02/23 12:29:47 ✅ Route+Perm registered → GET /api/export-pdf [finance:export] +2026/02/23 12:29:48 ✅ Route+Perm registered → GET /api/exportstamentheaderreport-pdf [finance:export] +2026/02/23 12:29:48 ✅ Route+Perm registered → GET /api/finance/customer-balances [finance:view] +2026/02/23 12:29:48 ✅ Route+Perm registered → GET /api/statements [finance:view] +2026/02/23 12:29:49 ✅ Route+Perm registered → GET /api/statements/{id}/details [finance:view] +2026/02/23 12:29:49 ✅ Route+Perm registered → POST /api/order/create [order:insert] +2026/02/23 12:29:50 ✅ Route+Perm registered → POST /api/order/update [order:update] +2026/02/23 12:29:50 ✅ Route+Perm registered → GET /api/order/get/{id} [order:view] +2026/02/23 12:29:51 ✅ Route+Perm registered → GET /api/orders/list [order:view] +2026/02/23 12:29:51 ✅ Route+Perm registered → GET /api/orders/production-list [order:update] +2026/02/23 12:29:51 ✅ Route+Perm registered → GET /api/orders/production-items/{id} [order:view] +2026/02/23 12:29:52 ✅ Route+Perm registered → POST /api/orders/production-items/{id}/insert-missing [order:update] +2026/02/23 12:29:52 ✅ Route+Perm registered → POST /api/orders/production-items/{id}/validate [order:update] +2026/02/23 12:29:53 ✅ Route+Perm registered → POST /api/orders/production-items/{id}/apply [order:update] +2026/02/23 12:29:53 ✅ Route+Perm registered → GET /api/orders/close-ready [order:update] +2026/02/23 12:29:54 ✅ Route+Perm registered → POST /api/orders/bulk-close [order:update] +2026/02/23 12:29:54 ✅ Route+Perm registered → GET /api/orders/export [order:export] +2026/02/23 12:29:54 ✅ Route+Perm registered → GET /api/order/check/{id} [order:view] +2026/02/23 12:29:55 ✅ Route+Perm registered → POST /api/order/validate [order:insert] +2026/02/23 12:29:55 ✅ Route+Perm registered → GET /api/order/pdf/{id} [order:export] +2026/02/23 12:29:56 ✅ Route+Perm registered → GET /api/order-inventory [order:view] +2026/02/23 12:29:56 ✅ Route+Perm registered → GET /api/orderpricelistb2b [order:view] +2026/02/23 12:29:57 ✅ Route+Perm registered → GET /api/min-price [order:view] +2026/02/23 12:29:57 ✅ Route+Perm registered → GET /api/products [order:view] +2026/02/23 12:29:57 ✅ Route+Perm registered → GET /api/product-detail [order:view] +2026/02/23 12:29:58 ✅ Route+Perm registered → GET /api/product-colors [order:view] +2026/02/23 12:29:58 ✅ Route+Perm registered → GET /api/product-colorsize [order:view] +2026/02/23 12:29:59 ✅ Route+Perm registered → GET /api/product-secondcolor [order:view] +2026/02/23 12:29:59 ✅ Route+Perm registered → GET /api/roles [user:view] +2026/02/23 12:29:59 ✅ Route+Perm registered → GET /api/departments [user:view] +2026/02/23 12:30:00 ✅ Route+Perm registered → GET /api/piyasalar [user:view] +2026/02/23 12:30:01 ✅ Route+Perm registered → POST /api/roles/{id}/departments [user:update] +2026/02/23 12:30:01 ✅ Route+Perm registered → POST /api/roles/{id}/piyasalar [user:update] +2026/02/23 12:30:01 ✅ Route+Perm registered → POST /api/users/{id}/roles [user:update] +2026/02/23 12:30:02 ✅ Route+Perm registered → POST /api/admin/users/{id}/piyasa-sync [admin:user.update] +2026/02/23 12:30:02 🌍 CORS Allowed Origin: http://ss.baggi.com.tr/app +2026/02/23 12:30:02 🚀 Server running at: 0.0.0.0:8080 +2026/02/23 12:30:43 ➡️ POST /api/auth/login | auth=false +2026/02/23 12:30:44 🔎 LOGIN DEBUG | mk_user_found=false err=mk_user not found hash_len=0 +2026/02/23 12:30:44 🟡 LEGACY LOGIN PATH: x +2026/02/23 12:30:44 🟡 LEGACY LOGIN QUERY HIT: x +2026/02/23 12:30:44 ❌ LEGACY SCAN ERROR: sql: no rows in result set +2026/02/23 12:30:44 ⬅️ POST /api/auth/login | status=401 | 279.0065ms +2026/02/23 12:30:44 ⚠️ LOGGER: claims is NIL +2026/02/23 12:30:44 🧾 auditlog INSERT | actor_dfusr= actor_user= role=public nav /api/auth/login target= +2026/02/23 12:30:50 ➡️ POST /api/auth/login | auth=false +2026/02/23 12:30:50 🧪 MK USER FROM DB +2026/02/23 12:30:50 🧪 ID=5 role_id=3 role_code='admin' depts=[UST_YONETIM] +2026/02/23 12:30:50 🔎 LOGIN DEBUG | mk_user_found=true err= hash_len=60 +2026/02/23 12:30:50 🧪 LOGIN RESPONSE USER DEBUG +2026/02/23 12:30:50 🧪 user.ID = 5 +2026/02/23 12:30:50 🧪 user.Username = mehmet.keçeci +2026/02/23 12:30:50 🧪 user.RoleID = 3 +2026/02/23 12:30:50 🧪 user.RoleCode = 'admin' +2026/02/23 12:30:50 🧪 user.IsActive = true +2026/02/23 12:30:50 ⬅️ POST /api/auth/login | status=200 | 593.239ms +2026/02/23 12:30:50 ⚠️ LOGGER: claims is NIL +2026/02/23 12:30:50 🧾 auditlog INSERT | actor_dfusr= actor_user= role=public nav /api/auth/login target= +2026/02/23 12:30:52 🔐 GLOBAL AUTH user=5 role=admin +2026/02/23 12:30:52 ➡️ GET /api/finance/customer-balances | auth=true +2026/02/23 12:30:52 AUTH_MIDDLEWARE PASS user=5 role=admin method=GET path=/api/finance/customer-balances +2026/02/23 12:30:52 🔐 PERM CHECK user=5 role=3 dept=[UST_YONETIM] finance:view +2026/02/23 12:30:53 ↳ ROLE+DEPT OVERRIDE = true +2026/02/23 12:33:21 ⬅️ GET /api/finance/customer-balances | status=200 | 2m28.8586087s +2026/02/23 12:33:21 ✅ LOGGER CLAIMS user=mehmet.keçeci role=admin id=5 +2026/02/23 12:33:21 🧾 auditlog INSERT | actor_dfusr=5 actor_user=mehmet.keçeci role=admin nav /api/finance/customer-balances target= +2026/02/23 13:40:17 ➡️ POST /api/auth/refresh | auth=false +2026/02/23 13:40:18 ⬅️ POST /api/auth/refresh | status=200 | 852.618ms +2026/02/23 13:40:18 ⚠️ LOGGER: claims is NIL +2026/02/23 13:40:18 🧾 auditlog INSERT | actor_dfusr= actor_user= role=public nav /api/auth/refresh target= +2026/02/23 13:40:18 🔐 GLOBAL AUTH user=5 role=admin +2026/02/23 13:40:18 ➡️ GET /api/finance/customer-balances | auth=true +2026/02/23 13:40:18 AUTH_MIDDLEWARE PASS user=5 role=admin method=GET path=/api/finance/customer-balances +2026/02/23 13:40:18 🔐 PERM CHECK user=5 role=3 dept=[UST_YONETIM] finance:view +2026/02/23 13:40:19 ↳ ROLE+DEPT OVERRIDE = true +2026/02/23 13:42:46 ⬅️ GET /api/finance/customer-balances | status=200 | 2m27.9525306s +2026/02/23 13:42:46 ✅ LOGGER CLAIMS user=mehmet.keçeci role=admin id=5 +2026/02/23 13:42:46 🧾 auditlog INSERT | actor_dfusr=5 actor_user=mehmet.keçeci role=admin nav /api/finance/customer-balances target= +exit status 1 diff --git a/ui/.quasar/dev-spa/app.js b/ui/.quasar/dev-spa/app.js deleted file mode 100644 index 608c77d..0000000 --- a/ui/.quasar/dev-spa/app.js +++ /dev/null @@ -1,77 +0,0 @@ -/* eslint-disable */ -/** - * THIS FILE IS GENERATED AUTOMATICALLY. - * DO NOT EDIT. - * - * You are probably looking on adding startup/initialization code. - * Use "quasar new boot " and add it there. - * One boot file per concern. Then reference the file(s) in quasar.config file > boot: - * boot: ['file', ...] // do not add ".js" extension to it. - * - * Boot files are your "main.js" - **/ - - - - - -import { Quasar } from 'quasar' -import { markRaw } from 'vue' -import RootComponent from 'app/src/App.vue' - -import createStore from 'app/src/stores/index' -import createRouter from 'app/src/router/index' - - - - - -export default async function (createAppFn, quasarUserOptions) { - - - // Create the app instance. - // Here we inject into it the Quasar UI, the router & possibly the store. - const app = createAppFn(RootComponent) - - - app.config.performance = true - - - app.use(Quasar, quasarUserOptions) - - - - - const store = typeof createStore === 'function' - ? await createStore({}) - : createStore - - - app.use(store) - - - - - - const router = markRaw( - typeof createRouter === 'function' - ? await createRouter({store}) - : createRouter - ) - - - // make router instance available in store - - store.use(({ store }) => { store.router = router }) - - - - // Expose the app, the router and the store. - // Note that we are not mounting the app here, since bootstrapping will be - // different depending on whether we are in a browser or on the server. - return { - app, - store, - router - } -} diff --git a/ui/.quasar/dev-spa/client-entry.js b/ui/.quasar/dev-spa/client-entry.js deleted file mode 100644 index 0adee13..0000000 --- a/ui/.quasar/dev-spa/client-entry.js +++ /dev/null @@ -1,156 +0,0 @@ -/* eslint-disable */ -/** - * THIS FILE IS GENERATED AUTOMATICALLY. - * DO NOT EDIT. - * - * You are probably looking on adding startup/initialization code. - * Use "quasar new boot " and add it there. - * One boot file per concern. Then reference the file(s) in quasar.config file > boot: - * boot: ['file', ...] // do not add ".js" extension to it. - * - * Boot files are your "main.js" - **/ - - -import { createApp } from 'vue' - - - - - - - -import '@quasar/extras/roboto-font/roboto-font.css' - -import '@quasar/extras/material-icons/material-icons.css' - - - - -// We load Quasar stylesheet file -import 'quasar/dist/quasar.sass' - - - - -import 'src/css/app.css' - - -import createQuasarApp from './app.js' -import quasarUserOptions from './quasar-user-options.js' - - - - - - -console.info('[Quasar] Running SPA.') - - - -const publicPath = `/` - - -async function start ({ - app, - router - , store -}, bootFiles) { - - let hasRedirected = false - const getRedirectUrl = url => { - try { return router.resolve(url).href } - catch (err) {} - - return Object(url) === url - ? null - : url - } - const redirect = url => { - hasRedirected = true - - if (typeof url === 'string' && /^https?:\/\//.test(url)) { - window.location.href = url - return - } - - const href = getRedirectUrl(url) - - // continue if we didn't fail to resolve the url - if (href !== null) { - window.location.href = href - window.location.reload() - } - } - - const urlPath = window.location.href.replace(window.location.origin, '') - - for (let i = 0; hasRedirected === false && i < bootFiles.length; i++) { - try { - await bootFiles[i]({ - app, - router, - store, - ssrContext: null, - redirect, - urlPath, - publicPath - }) - } - catch (err) { - if (err && err.url) { - redirect(err.url) - return - } - - console.error('[Quasar] boot error:', err) - return - } - } - - if (hasRedirected === true) return - - - app.use(router) - - - - - - - app.mount('#q-app') - - - -} - -createQuasarApp(createApp, quasarUserOptions) - - .then(app => { - // eventually remove this when Cordova/Capacitor/Electron support becomes old - const [ method, mapFn ] = Promise.allSettled !== void 0 - ? [ - 'allSettled', - bootFiles => bootFiles.map(result => { - if (result.status === 'rejected') { - console.error('[Quasar] boot error:', result.reason) - return - } - return result.value.default - }) - ] - : [ - 'all', - bootFiles => bootFiles.map(entry => entry.default) - ] - - return Promise[ method ]([ - - import(/* webpackMode: "eager" */ 'boot/dayjs') - - ]).then(bootFiles => { - const boot = mapFn(bootFiles).filter(entry => typeof entry === 'function') - start(app, boot) - }) - }) - diff --git a/ui/.quasar/dev-spa/client-prefetch.js b/ui/.quasar/dev-spa/client-prefetch.js deleted file mode 100644 index 9bbe3c5..0000000 --- a/ui/.quasar/dev-spa/client-prefetch.js +++ /dev/null @@ -1,116 +0,0 @@ -/* eslint-disable */ -/** - * THIS FILE IS GENERATED AUTOMATICALLY. - * DO NOT EDIT. - * - * You are probably looking on adding startup/initialization code. - * Use "quasar new boot " and add it there. - * One boot file per concern. Then reference the file(s) in quasar.config file > boot: - * boot: ['file', ...] // do not add ".js" extension to it. - * - * Boot files are your "main.js" - **/ - - - -import App from 'app/src/App.vue' -let appPrefetch = typeof App.preFetch === 'function' - ? App.preFetch - : ( - // Class components return the component options (and the preFetch hook) inside __c property - App.__c !== void 0 && typeof App.__c.preFetch === 'function' - ? App.__c.preFetch - : false - ) - - -function getMatchedComponents (to, router) { - const route = to - ? (to.matched ? to : router.resolve(to).route) - : router.currentRoute.value - - if (!route) { return [] } - - const matched = route.matched.filter(m => m.components !== void 0) - - if (matched.length === 0) { return [] } - - return Array.prototype.concat.apply([], matched.map(m => { - return Object.keys(m.components).map(key => { - const comp = m.components[key] - return { - path: m.path, - c: comp - } - }) - })) -} - -export function addPreFetchHooks ({ router, store, publicPath }) { - // Add router hook for handling preFetch. - // Doing it after initial route is resolved so that we don't double-fetch - // the data that we already have. Using router.beforeResolve() so that all - // async components are resolved. - router.beforeResolve((to, from, next) => { - const - urlPath = window.location.href.replace(window.location.origin, ''), - matched = getMatchedComponents(to, router), - prevMatched = getMatchedComponents(from, router) - - let diffed = false - const preFetchList = matched - .filter((m, i) => { - return diffed || (diffed = ( - !prevMatched[i] || - prevMatched[i].c !== m.c || - m.path.indexOf('/:') > -1 // does it has params? - )) - }) - .filter(m => m.c !== void 0 && ( - typeof m.c.preFetch === 'function' - // Class components return the component options (and the preFetch hook) inside __c property - || (m.c.__c !== void 0 && typeof m.c.__c.preFetch === 'function') - )) - .map(m => m.c.__c !== void 0 ? m.c.__c.preFetch : m.c.preFetch) - - - if (appPrefetch !== false) { - preFetchList.unshift(appPrefetch) - appPrefetch = false - } - - - if (preFetchList.length === 0) { - return next() - } - - let hasRedirected = false - const redirect = url => { - hasRedirected = true - next(url) - } - const proceed = () => { - - if (hasRedirected === false) { next() } - } - - - - preFetchList.reduce( - (promise, preFetch) => promise.then(() => hasRedirected === false && preFetch({ - store, - currentRoute: to, - previousRoute: from, - redirect, - urlPath, - publicPath - })), - Promise.resolve() - ) - .then(proceed) - .catch(e => { - console.error(e) - proceed() - }) - }) -} diff --git a/ui/.quasar/dev-spa/quasar-user-options.js b/ui/.quasar/dev-spa/quasar-user-options.js deleted file mode 100644 index ac1dae3..0000000 --- a/ui/.quasar/dev-spa/quasar-user-options.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -/** - * THIS FILE IS GENERATED AUTOMATICALLY. - * DO NOT EDIT. - * - * You are probably looking on adding startup/initialization code. - * Use "quasar new boot " and add it there. - * One boot file per concern. Then reference the file(s) in quasar.config file > boot: - * boot: ['file', ...] // do not add ".js" extension to it. - * - * Boot files are your "main.js" - **/ - -import lang from 'quasar/lang/tr.js' - - - -import {Loading,Dialog,Notify} from 'quasar' - - - -export default { config: {"notify":{"position":"top","timeout":2500}},lang,plugins: {Loading,Dialog,Notify} } - diff --git a/ui/.quasar/feature-flags.d.ts b/ui/.quasar/feature-flags.d.ts deleted file mode 100644 index dab07b3..0000000 --- a/ui/.quasar/feature-flags.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* eslint-disable */ -import "quasar/dist/types/feature-flag.d.ts"; - -declare module "quasar/dist/types/feature-flag.d.ts" { - interface QuasarFeatureFlags { - store: true; - } -} diff --git a/ui/.quasar/pinia.d.ts b/ui/.quasar/pinia.d.ts deleted file mode 100644 index 5bc0a53..0000000 --- a/ui/.quasar/pinia.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* eslint-disable */ -import { Router } from 'vue-router'; - -declare module 'pinia' { - export interface PiniaCustomProperties { - readonly router: Router; - } -} diff --git a/ui/.quasar/quasar.d.ts b/ui/.quasar/quasar.d.ts deleted file mode 100644 index e37de88..0000000 --- a/ui/.quasar/quasar.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -/* eslint-disable */ -/// - -/// diff --git a/ui/.quasar/tsconfig.json b/ui/.quasar/tsconfig.json deleted file mode 100644 index 99f52b3..0000000 --- a/ui/.quasar/tsconfig.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "compilerOptions": { - "esModuleInterop": true, - "skipLibCheck": true, - "target": "esnext", - "allowJs": true, - "resolveJsonModule": true, - "moduleDetection": "force", - "isolatedModules": true, - "module": "preserve", - "noEmit": true, - "lib": [ - "esnext", - "dom", - "dom.iterable" - ], - "paths": { - "src": [ - "./../src" - ], - "src/*": [ - "./../src/*" - ], - "app": [ - "./.." - ], - "app/*": [ - "./../*" - ], - "components": [ - "./../src/components" - ], - "components/*": [ - "./../src/components/*" - ], - "layouts": [ - "./../src/layouts" - ], - "layouts/*": [ - "./../src/layouts/*" - ], - "pages": [ - "./../src/pages" - ], - "pages/*": [ - "./../src/pages/*" - ], - "assets": [ - "./../src/assets" - ], - "assets/*": [ - "./../src/assets/*" - ], - "boot": [ - "./../src/boot" - ], - "boot/*": [ - "./../src/boot/*" - ], - "stores": [ - "./../src/stores" - ], - "stores/*": [ - "./../src/stores/*" - ], - "#q-app": [ - "./../node_modules/@quasar/app-webpack/types/index.d.ts" - ], - "#q-app/wrappers": [ - "./../node_modules/@quasar/app-webpack/types/app-wrappers.d.ts" - ], - "#q-app/bex/background": [ - "./../node_modules/@quasar/app-webpack/types/bex/entrypoints/background.d.ts" - ], - "#q-app/bex/content": [ - "./../node_modules/@quasar/app-webpack/types/bex/entrypoints/content.d.ts" - ], - "#q-app/bex/private/bex-bridge": [ - "./../node_modules/@quasar/app-webpack/types/bex/bex-bridge.d.ts" - ] - } - }, - "include": [ - "./**/*.d.ts", - "./../**/*" - ], - "exclude": [ - "./../dist", - "./../node_modules", - "./../src-capacitor", - "./../src-cordova", - "./../quasar.config.*.temporary.compiled*" - ] -} \ No newline at end of file diff --git a/ui/quasar.config.js.temporary.compiled.1771702670450.mjs b/ui/quasar.config.js.temporary.compiled.1771702670450.mjs deleted file mode 100644 index 4a501bd..0000000 --- a/ui/quasar.config.js.temporary.compiled.1771702670450.mjs +++ /dev/null @@ -1,125 +0,0 @@ -/* eslint-disable */ -/** - * THIS FILE IS GENERATED AUTOMATICALLY. - * 1. DO NOT edit this file directly as it won't do anything. - * 2. EDIT the original quasar.config file INSTEAD. - * 3. DO NOT git commit this file. It should be ignored. - * - * This file is still here because there was an error in - * the original quasar.config file and this allows you to - * investigate the Node.js stack error. - * - * After you fix the original file, this file will be - * deleted automatically. - **/ - - -// quasar.config.js -import { defineConfig } from "@quasar/app-webpack/wrappers"; -var quasar_config_default = defineConfig(() => { - const apiBaseUrl = (process.env.VITE_API_BASE_URL || "/api").trim(); - return { - /* ===================================================== - APP INFO - ===================================================== */ - productName: "Baggi BSS", - productDescription: "Baggi Tekstil Business Support System", - /* ===================================================== - BOOT FILES - ===================================================== */ - boot: ["dayjs"], - /* ===================================================== - GLOBAL CSS - ===================================================== */ - css: ["app.css"], - /* ===================================================== - ICONS / FONTS - ===================================================== */ - extras: [ - "roboto-font", - "material-icons" - ], - /* ===================================================== - BUILD (PRODUCTION) - ===================================================== */ - build: { - vueRouterMode: "hash", - env: { - VITE_API_BASE_URL: apiBaseUrl - }, - esbuildTarget: { - browser: ["es2022", "firefox115", "chrome115", "safari14"], - node: "node20" - }, - // Cache & performance - gzip: true, - preloadChunks: true - }, - /* ===================================================== - DEV SERVER (LOCAL) - ===================================================== */ - devServer: { - server: { type: "http" }, - port: 9e3, - open: true, - // DEV proxy (CORS'suz) - proxy: [ - { - context: ["/api"], - target: "http://localhost:8080", - changeOrigin: true, - secure: false - } - ] - }, - /* ===================================================== - QUASAR FRAMEWORK - ===================================================== */ - framework: { - config: { - notify: { - position: "top", - timeout: 2500 - } - }, - lang: "tr", - plugins: [ - "Loading", - "Dialog", - "Notify" - ] - }, - animations: [], - /* ===================================================== - SSR / PWA (DISABLED) - ===================================================== */ - ssr: { - prodPort: 3e3, - middlewares: ["render"], - pwa: false - }, - pwa: { - workboxMode: "GenerateSW" - }, - /* ===================================================== - MOBILE / DESKTOP - ===================================================== */ - capacitor: { - hideSplashscreen: true - }, - electron: { - preloadScripts: ["electron-preload"], - inspectPort: 5858, - bundler: "packager", - builder: { - appId: "baggisowtfaresystem" - } - }, - bex: { - extraScripts: [] - } - }; -}); -export { - quasar_config_default as default -}; diff --git a/ui/src/layouts/MainLayout.vue b/ui/src/layouts/MainLayout.vue index 551fca7..27386cc 100644 --- a/ui/src/layouts/MainLayout.vue +++ b/ui/src/layouts/MainLayout.vue @@ -195,6 +195,11 @@ const menuItems = [ label: 'Cari Ekstre', to: '/app/statementofaccount', permission: 'finance:view' + }, + { + label: 'Cari Bakiye Listesi', + to: '/app/customer-balance-list', + permission: 'finance:view' } ] }, diff --git a/ui/src/pages/CustomerBalanceList.vue b/ui/src/pages/CustomerBalanceList.vue new file mode 100644 index 0000000..785c18e --- /dev/null +++ b/ui/src/pages/CustomerBalanceList.vue @@ -0,0 +1,731 @@ + + + + + + + diff --git a/ui/src/pages/OrderEntry.vue b/ui/src/pages/OrderEntry.vue index 254d593..f423622 100644 --- a/ui/src/pages/OrderEntry.vue +++ b/ui/src/pages/OrderEntry.vue @@ -724,6 +724,13 @@ :disable="isClosedRow || isViewOnly || !canMutateRows" /> + { showEditor.value = false } +function normalizeColorValue(val) { + return String(val || '').trim().toUpperCase() +} + +function getNextColorValue() { + const options = Array.isArray(renkOptions.value) ? renkOptions.value : [] + if (!options.length) return null + + const current = normalizeColorValue(form.renk) + const idx = options.findIndex(o => normalizeColorValue(o.value) === current) + if (idx === -1) return null + + const next = options[idx + 1] + return next ? next.value : null +} + +const onSaveAndNextColor = async () => { + if (!hasRowMutationPermission()) { + notifyNoPermission( + isEditMode.value + ? 'Siparis satiri guncelleme yetkiniz yok' + : 'Siparis satiri kaydetme yetkiniz yok' + ) + return + } + + if (!form.model) { + $q.notify({ type: 'warning', message: 'Model seçiniz' }) + return + } + + if (!form.renk) { + $q.notify({ type: 'warning', message: 'Renk seçiniz' }) + return + } + + const ok = await orderStore.saveOrUpdateRowUnified({ + form, + recalcVat: typeof recalcVat === 'function' ? recalcVat : null, + resetEditor: () => {}, + stockMap, + $q + }) + + if (!ok) return + + // Edit state temizle: renk değişimi combo delete tetiklemesin + orderStore.editingKey = null + orderStore.selected = null + + const nextColor = getNextColorValue() + if (!nextColor) { + $q.notify({ + type: 'warning', + message: 'Son renktesiniz. Lütfen farklı bir renk seçin.', + position: 'top-right' + }) + return + } + + form.renk2 = '' + await onColorChange(nextColor) + + $q.notify({ + type: 'info', + message: 'Satır kaydedildi. Bir sonraki renge geçildi.', + position: 'top-right' + }) +} + diff --git a/ui/src/router/routes.js b/ui/src/router/routes.js index 7590bd5..124a967 100644 --- a/ui/src/router/routes.js +++ b/ui/src/router/routes.js @@ -134,6 +134,13 @@ const routes = [ meta: { permission: 'finance:view' } }, + { + path: 'customer-balance-list', + name: 'customer-balance-list', + component: () => import('pages/CustomerBalanceList.vue'), + meta: { permission: 'finance:view' } + }, + /* ================= USERS ================= */ diff --git a/ui/src/stores/customerBalanceListStore.js b/ui/src/stores/customerBalanceListStore.js new file mode 100644 index 0000000..c327726 --- /dev/null +++ b/ui/src/stores/customerBalanceListStore.js @@ -0,0 +1,262 @@ +import { defineStore } from 'pinia' +import api from 'src/services/api' + +export const useCustomerBalanceListStore = defineStore('customerBalanceList', { + state: () => ({ + filters: { + selectedDate: new Date().toISOString().slice(0, 10), + cariSearch: '', + appliedCariSearch: '', + cariIlkGrup: [], + piyasa: [], + temsilci: [], + riskDurumu: [], + islemTipi: [], + ulke: [] + }, + rows: [], + loading: false, + error: null, + hasFetched: false, + defaultsInitialized: false + }), + + getters: { + cariIlkGrupOptions: (state) => uniqueOptions(state.rows, 'cari_ilk_grup'), + piyasaOptions: (state) => uniqueOptions(state.rows, 'piyasa'), + temsilciOptions: (state) => uniqueOptions(state.rows, 'temsilci'), + riskDurumuOptions: (state) => uniqueOptions(state.rows, 'ozellik03'), + ulkeOptions: (state) => uniqueOptions(state.rows, 'ozellik05'), + + filteredRows: (state) => { + return state.rows.filter((row) => { + const cariIlkGrupOk = + !state.filters.cariIlkGrup.length || + state.filters.cariIlkGrup.includes(row.cari_ilk_grup) + + const piyasaOk = + !state.filters.piyasa.length || + state.filters.piyasa.includes(row.piyasa) + + const temsilciOk = + !state.filters.temsilci.length || + state.filters.temsilci.includes(row.temsilci) + + const riskDurumuOk = + !state.filters.riskDurumu.length || + state.filters.riskDurumu.includes(row.ozellik03) + + const cariText = normalizeText([ + row.ana_cari_kodu || '', + row.ana_cari_adi || '', + row.cari_kodu || '', + row.cari_detay || '' + ].join(' ')) + const cariSearchNeedle = normalizeText(state.filters.appliedCariSearch || '') + const cariSearchOk = + !cariSearchNeedle || + cariText.includes(cariSearchNeedle) + + const ulkeOk = + !state.filters.ulke.length || + state.filters.ulke.includes(row.ozellik05) + + const islemTipiOk = + !state.filters.islemTipi.length || + state.filters.islemTipi.some((t) => { + const bak12 = Number(row.bakiye_1_2) || 0 + const bak13 = Number(row.bakiye_1_3) || 0 + const usd12 = Number(row.usd_bakiye_1_2) || 0 + const try12 = Number(row.tl_bakiye_1_2) || 0 + const usd13 = Number(row.usd_bakiye_1_3) || 0 + const try13 = Number(row.tl_bakiye_1_3) || 0 + + if (t === 'prbr_1_2') return bak12 !== 0 + if (t === 'prbr_1_3') return bak13 !== 0 + if (t === 'usd_1_2') return usd12 !== 0 + if (t === 'try_1_2') return try12 !== 0 + if (t === 'usd_1_3') return usd13 !== 0 + if (t === 'try_1_3') return try13 !== 0 + return false + }) + + return cariIlkGrupOk && piyasaOk && temsilciOk && riskDurumuOk && cariSearchOk && ulkeOk && islemTipiOk + }) + }, + + summaryRows () { + const grouped = new Map() + + for (const row of this.filteredRows) { + const key = `${row.ana_cari_kodu || ''}||${row.ana_cari_adi || ''}` + const current = grouped.get(key) || { + group_key: key, + ana_cari_kodu: row.ana_cari_kodu || '', + ana_cari_adi: row.ana_cari_adi || '', + piyasa: '', + piyasa_set: new Set(), + temsilci: '', + temsilci_set: new Set(), + risk_durumu: '', + risk_set: new Set(), + bakiye_1_2_map: {}, + bakiye_1_3_map: {}, + usd_bakiye_1_2: 0, + tl_bakiye_1_2: 0, + usd_bakiye_1_3: 0, + tl_bakiye_1_3: 0, + kalan_fatura_ortalama_vade_tarihi: '' + } + + current.usd_bakiye_1_2 += Number(row.usd_bakiye_1_2) || 0 + current.tl_bakiye_1_2 += Number(row.tl_bakiye_1_2) || 0 + current.usd_bakiye_1_3 += Number(row.usd_bakiye_1_3) || 0 + current.tl_bakiye_1_3 += Number(row.tl_bakiye_1_3) || 0 + + const curr = String(row.cari_doviz || '').trim().toUpperCase() || 'N/A' + current.bakiye_1_2_map[curr] = + (Number(current.bakiye_1_2_map[curr]) || 0) + (Number(row.bakiye_1_2) || 0) + current.bakiye_1_3_map[curr] = + (Number(current.bakiye_1_3_map[curr]) || 0) + (Number(row.bakiye_1_3) || 0) + + const piyasa = String(row.piyasa || '').trim() + if (piyasa) current.piyasa_set.add(piyasa) + + const temsilci = String(row.temsilci || '').trim() + if (temsilci) current.temsilci_set.add(temsilci) + + if ( + !current.kalan_fatura_ortalama_vade_tarihi && + row.kalan_fatura_ortalama_vade_tarihi + ) { + current.kalan_fatura_ortalama_vade_tarihi = row.kalan_fatura_ortalama_vade_tarihi + } + + const risk = String(row.ozellik03 || '').trim() + if (risk) current.risk_set.add(risk) + + const riskValues = Array.from(current.risk_set) + current.risk_durumu = + riskValues.length <= 1 + ? (riskValues[0] || '-') + : riskValues.join(', ') + + const piyasaValues = Array.from(current.piyasa_set) + current.piyasa = + piyasaValues.length <= 1 + ? (piyasaValues[0] || '-') + : piyasaValues.join(', ') + + const temsilciValues = Array.from(current.temsilci_set) + current.temsilci = + temsilciValues.length <= 1 + ? (temsilciValues[0] || '-') + : temsilciValues.join(', ') + + grouped.set(key, current) + } + + return Array.from(grouped.values()).map((r) => { + const { risk_set, piyasa_set, temsilci_set, ...rest } = r + return rest + }) + } + }, + + actions: { + async fetchCustomerBalances () { + this.loading = true + this.error = null + + try { + const { data } = await api.get('/finance/customer-balances', { + params: { + selected_date: this.filters.selectedDate, + cari_search: String(this.filters.appliedCariSearch || this.filters.cariSearch || '').trim() + } + }) + this.rows = Array.isArray(data) ? data : [] + if (!this.defaultsInitialized) { + this.applyInitialFilterDefaults() + this.defaultsInitialized = true + } + this.hasFetched = true + } catch (err) { + this.rows = [] + this.hasFetched = false + this.error = + err?.response?.data?.message || + err?.message || + 'Cari bakiye listesi getirilemedi.' + } finally { + this.loading = false + } + }, + + getDetailsByGroup (groupKey) { + return this.filteredRows.filter(r => + `${r.ana_cari_kodu || ''}||${r.ana_cari_adi || ''}` === groupKey + ) + }, + + resetFilters () { + this.filters.cariSearch = '' + this.filters.appliedCariSearch = '' + this.filters.cariIlkGrup = [] + this.filters.piyasa = [] + this.filters.temsilci = [] + this.filters.riskDurumu = [] + this.filters.islemTipi = [] + this.filters.ulke = [] + this.defaultsInitialized = false + }, + + applyCariSearch () { + this.filters.appliedCariSearch = String(this.filters.cariSearch || '').trim() + }, + + selectAll (field, options) { + this.filters[field] = options.map(o => o.value) + }, + + clearAll (field) { + this.filters[field] = [] + }, + + applyInitialFilterDefaults () { + const transferKey = normalizeText('transfer') + this.filters.cariIlkGrup = this.cariIlkGrupOptions + .map(o => o.value) + .filter(v => normalizeText(v) !== transferKey) + + const excludedRisk = new Set([ + normalizeText('avukat'), + normalizeText('orta risk'), + normalizeText('yuksek risk') + ]) + this.filters.riskDurumu = this.riskDurumuOptions + .map(o => o.value) + .filter(v => !excludedRisk.has(normalizeText(v))) + } + } +}) + +function uniqueOptions (rows, field) { + const set = new Set() + for (const r of rows) { + const v = String(r[field] || '').trim() + if (v) set.add(v) + } + + return Array.from(set) + .sort((a, b) => a.localeCompare(b, 'tr')) + .map(v => ({ label: v, value: v })) +} + +function normalizeText (str) { + return String(str || '') + .toLocaleLowerCase('tr-TR') + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim() +}