Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-02-25 10:40:07 +03:00
parent 47848fc14d
commit 15e51e9c39
21 changed files with 1526 additions and 618 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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)
}
}

137
svc/run.log Normal file
View File

@@ -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=<nil> actor_user=<nil> role=public nav /api/auth/login target=<nil>
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=<nil> 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=<nil> actor_user=<nil> role=public nav /api/auth/login target=<nil>
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=<nil>
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=<nil> actor_user=<nil> role=public nav /api/auth/refresh target=<nil>
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=<nil>
exit status 1

View File

@@ -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 <name>" 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
}
}

View File

@@ -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 <name>" 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)
})
})

View File

@@ -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 <name>" 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()
})
})
}

View File

@@ -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 <name>" 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} }

View File

@@ -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;
}
}

View File

@@ -1,8 +0,0 @@
/* eslint-disable */
import { Router } from 'vue-router';
declare module 'pinia' {
export interface PiniaCustomProperties {
readonly router: Router;
}
}

View File

@@ -1,4 +0,0 @@
/* eslint-disable */
/// <reference types="@quasar/app-webpack" />
/// <reference types="vite/client" />

View File

@@ -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*"
]
}

View File

@@ -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
};

View File

@@ -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'
}
]
},

View File

@@ -0,0 +1,731 @@
<template>
<q-page v-if="canReadFinance" class="q-pa-md page-layout">
<div class="filter-sticky">
<div class="row q-col-gutter-sm q-mb-md">
<div class="col-12 col-sm-6 col-md-4">
<q-input
v-model="store.filters.cariSearch"
filled
dense
label="Cari Kodu / Cari Adı"
@keyup.enter="store.applyCariSearch()"
>
<template #append>
<q-btn
dense
flat
round
icon="search"
@click="store.applyCariSearch()"
/>
</template>
</q-input>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-input
v-model="store.filters.selectedDate"
label="Tarih"
filled
dense
readonly
>
<template #append>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-date v-model="store.filters.selectedDate" mask="YYYY-MM-DD" />
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</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 (Özellik05)"
: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>
<div class="row q-col-gutter-sm q-mb-md">
<div class="col-auto">
<q-btn
color="primary"
icon="download"
label="Bakiyeleri Getir"
:loading="store.loading"
@click="store.fetchCustomerBalances()"
/>
</div>
<div class="col-auto">
<q-btn
flat
color="grey-8"
icon="restart_alt"
label="Sıfırla"
@click="onReset"
/>
</div>
</div>
<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">
Bakiyeleri Getir Tuşuna Basmadan Sistem Çalışmaz
</q-banner>
</div>
<div class="table-area">
<div class="sticky-bar row justify-end items-center q-pa-sm bg-grey-1">
<q-btn
flat
color="secondary"
icon="list"
:label="allDetailsOpen ? 'Tüm Detayları Kapat' : 'Tüm Detayları Aç'"
@click="toggleAllDetails"
/>
</div>
<q-table
title="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="col.name === 'hesap_alinmayan_gun'" class="text-right block">
-
</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>
</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 module erisim yetkiniz yok.
</div>
</q-page>
</template>
<script setup>
import { computed, ref } from 'vue'
import { useCustomerBalanceListStore } from 'src/stores/customerBalanceListStore'
import { usePermission } from 'src/composables/usePermission'
const store = useCustomerBalanceListStore()
const expanded = ref({})
const allDetailsOpen = ref(false)
const { canRead } = usePermission()
const canReadFinance = canRead('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 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 },
try_1_2: { name: 'tl_bakiye_1_2', label: '1_2 TRY_BAKIYE', field: 'tl_bakiye_1_2', align: 'center', sortable: true },
usd_1_3: { name: 'usd_bakiye_1_3', label: '1_3 USD_BAKIYE', field: 'usd_bakiye_1_3', align: 'center', sortable: true },
try_1_3: { name: 'tl_bakiye_1_3', label: '1_3 TRY_BAKIYE', field: 'tl_bakiye_1_3', align: 'center', sortable: true }
}
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 },
{ name: 'ana_cari_adi', label: 'Ana Cari Detay', field: 'ana_cari_adi', align: 'left', sortable: true },
{ name: 'piyasa', label: 'Piyasa', field: 'piyasa', align: 'left', sortable: true },
{ name: 'temsilci', label: 'Temsilci', field: 'temsilci', align: 'left', sortable: true },
{ name: 'risk_durumu', label: 'Risk Durumu', field: 'risk_durumu', align: 'left', sortable: true },
...selectedMetricKeys.value.map(k => metricDefs[k]),
{ name: 'hesap_alinmayan_gun', label: 'Hesap Alınmayan Gün', field: 'hesap_alinmayan_gun', align: 'right', sortable: false },
{ name: 'kalan_fatura_ortalama_vade_tarihi', label: 'Kalan Fatura Ortalama Vade Tarihi', field: 'kalan_fatura_ortalama_vade_tarihi', align: 'left', sortable: true }
]))
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
return acc
}, {
usd_bakiye_1_2: 0,
tl_bakiye_1_2: 0,
usd_bakiye_1_3: 0,
tl_bakiye_1_3: 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: 'piyasa', label: 'Piyasa', field: 'piyasa', align: 'left' },
{ name: 'temsilci', label: 'Temsilci', field: 'temsilci', align: 'left' },
{ name: 'ozellik03', label: 'Risk Durumu', field: 'ozellik03', align: 'left' },
{ name: 'ozellik05', label: 'Ülke', field: 'ozellik05', align: 'left' },
{ name: 'ozellik06', label: 'Özellik06', field: 'ozellik06', align: 'left' },
{ name: 'ozellik07', label: 'Özellik07', field: 'ozellik07', align: 'left' },
{ name: 'cari_doviz', label: 'Döviz', field: 'cari_doviz', align: 'left' },
...selectedMetricKeys.value.map(k => metricDefs[k]),
{ name: 'hesap_alinmayan_gun', label: 'Hesap Alınmayan Gün', field: 'hesap_alinmayan_gun', align: 'right' },
{ name: 'kalan_fatura_ortalama_vade_tarihi', label: 'Kalan Fatura Ortalama Vade Tarihi', field: 'kalan_fatura_ortalama_vade_tarihi', align: 'left' }
])
function onReset () {
store.resetFilters()
store.applyCariSearch()
}
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 = {}
}
function formatAmount (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 === 'hesap_alinmayan_gun') return '-'
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(' | ')
}
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)}`
}
</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;
}
.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: normal;
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

@@ -724,6 +724,13 @@
:disable="isClosedRow || isViewOnly || !canMutateRows"
/>
<q-btn
v-if="canMutateRows"
color="secondary"
label="Kaydet ve Diğer Renge Geç"
@click="onSaveAndNextColor"
:disable="isClosedRow || isViewOnly || !canMutateRows"
/>
<q-btn
v-if="isEditing && canMutateRows"
color="negative"
@@ -2831,6 +2838,76 @@ const onSaveOrUpdateRow = async () => {
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'
})
}

View File

@@ -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 ================= */

View File

@@ -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()
}