From aba71341b997fe65da41949a69b930f7e7a9d87f Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Tue, 10 Mar 2026 17:51:31 +0300 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- svc/internal/authz/mssql.go | 35 +---- svc/main.go | 10 ++ svc/middlewares/authz_v2.go | 56 +++++++ svc/queries/account.go | 6 +- svc/queries/statement_aging.go | 9 +- svc/routes/customer_balance_pdf.go | 155 ++++++++++++++----- svc/routes/login.go | 15 +- svc/routes/statement_aging_pdf.go | 1 + ui/src/pages/AccountAgingStatement.vue | 6 +- ui/src/pages/AgingCustomerBalancelist.go.vue | 8 +- ui/src/pages/CustomerBalanceList.vue | 8 +- ui/src/pages/OrderEntry.vue | 13 +- ui/src/pages/OrderProductionUpdate.vue | 9 +- ui/src/pages/ProductStockByAttributes.vue | 30 ++-- ui/src/pages/ProductStockQuery.vue | 7 +- ui/src/pages/ProductionWorkerGateway.vue | 6 +- ui/src/pages/StatementReport.vue | 5 +- ui/src/pages/UserDetail.vue | 5 - ui/src/pages/statementofaccount.vue | 3 - ui/src/router/index.js | 7 - ui/src/stores/UserDetailStore.js | 21 --- ui/src/stores/authStore.js | 12 -- ui/src/stores/statementAgingStore.js | 25 ++- ui/src/utils/searchText.js | 7 + 24 files changed, 299 insertions(+), 160 deletions(-) create mode 100644 ui/src/utils/searchText.js diff --git a/svc/internal/authz/mssql.go b/svc/internal/authz/mssql.go index 498bbb0..77fb71a 100644 --- a/svc/internal/authz/mssql.go +++ b/svc/internal/authz/mssql.go @@ -4,7 +4,6 @@ import ( "bssapp-backend/auth" "context" "fmt" - "strings" ) func BuildMSSQLPiyasaFilter( @@ -27,37 +26,5 @@ func BuildMSSQLPiyasaFilter( func BuildMSSQLPiyasaFilterWithCodes(column string, codes []string) string { normalizedCol := fmt.Sprintf("UPPER(LTRIM(RTRIM(%s)))", column) - - exact := BuildINClause(normalizedCol, codes) - prefixCodes := first3Codes(codes) - if len(prefixCodes) == 0 { - return exact - } - - prefix := BuildINClause( - fmt.Sprintf("LEFT(%s, 3)", normalizedCol), - prefixCodes, - ) - - return fmt.Sprintf("(%s OR %s)", exact, prefix) -} - -func first3Codes(codes []string) []string { - seen := make(map[string]struct{}, len(codes)) - out := make([]string, 0, len(codes)) - - for _, c := range codes { - n := strings.ToUpper(strings.TrimSpace(c)) - if len(n) < 3 { - continue - } - n = n[:3] - if _, ok := seen[n]; ok { - continue - } - seen[n] = struct{}{} - out = append(out, n) - } - - return out + return BuildINClause(normalizedCol, codes) } diff --git a/svc/main.go b/svc/main.go index dc8ba50..7e818a4 100644 --- a/svc/main.go +++ b/svc/main.go @@ -571,6 +571,16 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "order", "view", wrapV3(http.HandlerFunc(routes.GetProductStockQueryByAttributesHandler)), ) + bindV3(r, pgDB, + "/api/product-images", "GET", + "order", "view", + wrapV3(http.HandlerFunc(routes.GetProductImagesHandler(pgDB))), + ) + bindV3(r, pgDB, + "/api/product-images/{id}/content", "GET", + "order", "view", + wrapV3(http.HandlerFunc(routes.GetProductImageContentHandler(pgDB))), + ) // ============================================================ // ROLE MANAGEMENT diff --git a/svc/middlewares/authz_v2.go b/svc/middlewares/authz_v2.go index abb9130..25f52b5 100644 --- a/svc/middlewares/authz_v2.go +++ b/svc/middlewares/authz_v2.go @@ -70,6 +70,13 @@ var routeMetaFallback = map[string]routeMeta{ "GET /api/product-stock-query-by-attributes": {module: "order", action: "view"}, } +var userLookupPaths = map[string]struct{}{ + "/api/lookups/roles": {}, + "/api/lookups/departments": {}, + "/api/lookups/piyasalar": {}, + "/api/lookups/nebim-users": {}, +} + // ===================================================== // 🌍 GLOBAL SCOPE CACHE (for invalidation) // ===================================================== @@ -859,6 +866,36 @@ func intersect(a, b []string) []string { return out } + +func isUserLookupPath(pathTemplate string) bool { + _, ok := userLookupPaths[pathTemplate] + return ok +} + +func resolveAnyUserCrudPermission( + repo *permissions.PermissionRepository, + userID int64, + roleID int64, + departmentCodes []string, +) (bool, error) { + for _, action := range []string{"view", "insert", "update"} { + allowed, err := repo.ResolvePermissionChain( + userID, + roleID, + departmentCodes, + "user", + action, + ) + if err != nil { + return false, err + } + if allowed { + return true, nil + } + } + return false, nil +} + func AuthzGuardByRoute(pg *sql.DB) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { @@ -993,6 +1030,25 @@ func AuthzGuardByRoute(pg *sql.DB) func(http.Handler) http.Handler { return } + if !allowed && isUserLookupPath(pathTemplate) { + allowed, err = resolveAnyUserCrudPermission( + repo, + int64(claims.ID), + int64(claims.RoleID), + claims.DepartmentCodes, + ) + if err != nil { + log.Printf( + "❌ AUTHZ: user lookup fallback resolve error user=%d path=%s err=%v", + claims.ID, + pathTemplate, + err, + ) + http.Error(w, "forbidden", http.StatusForbidden) + return + } + } + if !allowed { log.Printf( diff --git a/svc/queries/account.go b/svc/queries/account.go index 647b314..7e91c85 100644 --- a/svc/queries/account.go +++ b/svc/queries/account.go @@ -26,7 +26,7 @@ func GetAccounts(ctx context.Context) ([]models.Account, error) { ;WITH VendorPiyasa AS ( SELECT - Cari8 = LEFT(P.CurrAccCode, 8), + Cari8 = LEFT(REPLACE(P.CurrAccCode, ' ', ''), 8), VendorAtt01 = MAX(P.VendorAtt01) FROM ( @@ -53,7 +53,7 @@ func GetAccounts(ctx context.Context) ([]models.Account, error) { ) pvt GROUP BY CurrAccTypeCode, CurrAccCode ) P - GROUP BY LEFT(P.CurrAccCode, 8) + GROUP BY LEFT(REPLACE(P.CurrAccCode, ' ', ''), 8) ) SELECT x.AccountCode, @@ -71,7 +71,7 @@ func GetAccounts(ctx context.Context) ([]models.Account, error) { ON f2.CurrAccTypeCode = b.CurrAccTypeCode AND f2.CurrAccCode = b.CurrAccCode LEFT JOIN VendorPiyasa vp - ON vp.Cari8 = LEFT(b.CurrAccCode, 8) + ON vp.Cari8 = LEFT(REPLACE(b.CurrAccCode, ' ', ''), 8) WHERE b.CurrAccTypeCode IN (1,3) AND %s ) x diff --git a/svc/queries/statement_aging.go b/svc/queries/statement_aging.go index be2d716..7571643 100644 --- a/svc/queries/statement_aging.go +++ b/svc/queries/statement_aging.go @@ -104,10 +104,17 @@ func GetStatementAging(params models.StatementAgingParams) ([]map[string]interfa tutar := asFloat64(row["EslesenTutar"]) usdTutar := toUSD(tutar, curr, usdTry, rateMap) + currTry := rateMap[curr] + usdToCurr := 0.0 + if currTry > 0 && usdTry > 0 { + usdToCurr = usdTry / currTry + } row["CariDetay"] = cariDetailMap[cari8] row["UsdTutar"] = round2(usdTutar) - row["CurrencyTryRate"] = round6(rateMap[curr]) + row["CurrencyTryRate"] = round6(currTry) + row["UsdTryRate"] = round6(usdTry) + row["CurrencyUsdRate"] = round6(usdToCurr) } return result, nil diff --git a/svc/routes/customer_balance_pdf.go b/svc/routes/customer_balance_pdf.go index 3ddb075..ffa81e0 100644 --- a/svc/routes/customer_balance_pdf.go +++ b/svc/routes/customer_balance_pdf.go @@ -86,6 +86,7 @@ func ExportCustomerBalancePDFHandler(_ *sql.DB) http.HandlerFunc { params.CariSearch, detailed, "Cari Bakiye Listesi", + false, summaries, detailsByMaster, ) @@ -235,34 +236,51 @@ func drawCustomerBalancePDF( searchText string, detailed bool, reportTitle string, + includeVadeColumns bool, summaries []balanceSummaryPDF, detailsByMaster map[string][]models.CustomerBalanceListRow, ) { - pageW, _ := pdf.GetPageSize() + pageW, pageH := pdf.GetPageSize() marginL, marginT, marginR, marginB := 8.0, 8.0, 8.0, 12.0 tableW := pageW - marginL - marginR summaryCols := []string{"Ana Cari Kod", "Ana Cari Detay", "Piyasa", "Temsilci", "Risk", "1_2 Pr.Br", "1_3 Pr.Br", "1_2 USD", "1_2 TRY", "1_3 USD", "1_3 TRY"} - summaryW := normalizeWidths([]float64{20, 43, 18, 18, 16, 27, 27, 15, 15, 15, 15}, tableW) + summaryWeights := []float64{18, 42, 16, 16, 14, 24, 24, 14, 14, 14, 14} + if includeVadeColumns { + summaryCols = append(summaryCols, "Vade Gun", "Belge Tarihi Gun") + summaryWeights = append(summaryWeights, 12, 16) + } + summaryW := normalizeWidths(summaryWeights, tableW) detailCols := []string{"Cari Kod", "Cari Detay", "Sirket", "Muhasebe", "Doviz", "1_2 Pr.Br", "1_3 Pr.Br", "1_2 USD", "1_2 TRY", "1_3 USD", "1_3 TRY"} - detailW := normalizeWidths([]float64{26, 46, 10, 20, 10, 24, 24, 15, 15, 15, 15}, tableW) + detailWeights := []float64{23, 40, 9, 18, 9, 20, 20, 13, 13, 13, 13} + if includeVadeColumns { + detailCols = append(detailCols, "Vade Gun", "Belge Tarihi Gun") + detailWeights = append(detailWeights, 11, 14) + } + detailW := normalizeWidths(detailWeights, tableW) header := func() { pdf.AddPage() + titleX := marginL + if logoPath, err := resolvePdfImagePath("Baggi-Tekstil-A.s-Logolu.jpeg"); err == nil { + pdf.ImageOptions(logoPath, marginL, marginT-1, 34, 0, false, gofpdf.ImageOptions{}, 0, "") + titleX = marginL + 38 + } + pdf.SetFont("dejavu", "B", 15) pdf.SetTextColor(149, 113, 22) - pdf.SetXY(marginL, marginT) + pdf.SetXY(titleX, marginT) title := strings.TrimSpace(reportTitle) if title == "" { title = "Cari Bakiye Listesi" } - pdf.CellFormat(120, 7, title, "", 0, "L", false, 0, "") + pdf.CellFormat(140, 7, title, "", 0, "L", false, 0, "") pdf.SetFont("dejavu", "", 9) pdf.SetTextColor(20, 20, 20) pdf.SetXY(pageW-marginR-80, marginT+1) - pdf.CellFormat(80, 5, "Tarih: "+selectedDate, "", 0, "R", false, 0, "") + pdf.CellFormat(80, 5, "Tarih: "+formatDateTR(selectedDate), "", 0, "R", false, 0, "") mode := "Detaysiz" if detailed { @@ -272,8 +290,8 @@ func drawCustomerBalancePDF( pdf.CellFormat(80, 5, "Mod: "+mode, "", 0, "R", false, 0, "") if strings.TrimSpace(searchText) != "" { - pdf.SetXY(marginL, marginT+8) - pdf.CellFormat(tableW, 5, "Arama: "+searchText, "", 0, "L", false, 0, "") + pdf.SetXY(titleX, marginT+8) + pdf.CellFormat(tableW-(titleX-marginL), 5, "Arama: "+searchText, "", 0, "L", false, 0, "") } pdf.SetDrawColor(149, 113, 22) @@ -283,7 +301,7 @@ func drawCustomerBalancePDF( } needPage := func(needH float64) bool { - return pdf.GetY()+needH+marginB > 210.0 + return pdf.GetY()+needH+marginB > pageH } drawSummaryHeader := func() { @@ -323,11 +341,6 @@ func drawCustomerBalancePDF( pdf.SetTextColor(20, 20, 20) for _, s := range summaries { - if needPage(6.2) { - header() - drawSummaryHeader() - } - row := []string{ s.AnaCariKodu, s.AnaCariAdi, @@ -341,20 +354,31 @@ func drawCustomerBalancePDF( formatMoneyPDF(s.USDBakiye13), formatMoneyPDF(s.TLBakiye13), } + if includeVadeColumns { + row = append(row, formatMoneyPDF(s.VadeGun), formatMoneyPDF(s.VadeBelge)) + } + + rowH := calcPDFRowHeight(pdf, row, summaryW, map[int]bool{1: true, 2: true, 3: true}, 6.2, 3.6) + if needPage(rowH) { + header() + drawSummaryHeader() + } y := pdf.GetY() x := marginL for i, v := range row { - pdf.Rect(x, y, summaryW[i], 6.2, "") + pdf.Rect(x, y, summaryW[i], rowH, "") align := "L" if i >= 7 { align = "R" } - pdf.SetXY(x+1, y+1) - pdf.CellFormat(summaryW[i]-2, 4.2, v, "", 0, align, false, 0, "") + if includeVadeColumns && (i == len(row)-1 || i == len(row)-2) { + align = "C" + } + drawPDFCellWrapped(pdf, v, x, y, summaryW[i], rowH, align, 3.6) x += summaryW[i] } - pdf.SetY(y + 6.2) + pdf.SetY(y + rowH) } if !detailed { @@ -386,7 +410,25 @@ func drawCustomerBalancePDF( pdf.SetTextColor(40, 40, 40) for _, r := range rows { - if needPage(5.8) { + line := []string{ + r.CariKodu, + r.CariDetay, + r.Sirket, + r.MuhasebeKodu, + r.CariDoviz, + formatMoneyPDF(r.Bakiye12), + formatMoneyPDF(r.Bakiye13), + formatMoneyPDF(r.USDBakiye12), + formatMoneyPDF(r.TLBakiye12), + formatMoneyPDF(r.USDBakiye13), + formatMoneyPDF(r.TLBakiye13), + } + if includeVadeColumns { + line = append(line, formatMoneyPDF(r.VadeGun), formatMoneyPDF(r.VadeBelgeGun)) + } + + rowH := calcPDFRowHeight(pdf, line, detailW, map[int]bool{1: true}, 5.8, 3.3) + if needPage(rowH) { header() pdf.SetFont("dejavu", "B", 8) pdf.SetFillColor(218, 193, 151) @@ -401,38 +443,77 @@ func drawCustomerBalancePDF( pdf.SetTextColor(40, 40, 40) } - line := []string{ - r.CariKodu, - r.CariDetay, - r.Sirket, - r.MuhasebeKodu, - r.CariDoviz, - formatMoneyPDF(r.Bakiye12), - formatMoneyPDF(r.Bakiye13), - formatMoneyPDF(r.USDBakiye12), - formatMoneyPDF(r.TLBakiye12), - formatMoneyPDF(r.USDBakiye13), - formatMoneyPDF(r.TLBakiye13), - } - rowY := pdf.GetY() rowX := marginL for i, v := range line { - pdf.Rect(rowX, rowY, detailW[i], 5.8, "") + pdf.Rect(rowX, rowY, detailW[i], rowH, "") align := "L" if i >= 5 { align = "R" } - pdf.SetXY(rowX+1, rowY+0.8) - pdf.CellFormat(detailW[i]-2, 4.0, v, "", 0, align, false, 0, "") + if includeVadeColumns && (i == len(line)-1 || i == len(line)-2) { + align = "C" + } + drawPDFCellWrapped(pdf, v, rowX, rowY, detailW[i], rowH, align, 3.3) rowX += detailW[i] } - pdf.SetY(rowY + 5.8) + pdf.SetY(rowY + rowH) } pdf.Ln(1.2) } } +func formatDateTR(v string) string { + s := strings.TrimSpace(v) + if s == "" { + return s + } + if t, err := time.Parse("2006-01-02", s); err == nil { + return t.Format("02.01.2006") + } + if t, err := time.Parse(time.RFC3339, s); err == nil { + return t.Format("02.01.2006") + } + return s +} + +func calcPDFRowHeight(pdf *gofpdf.Fpdf, row []string, widths []float64, wrapIdx map[int]bool, minH, lineH float64) float64 { + maxLines := 1 + for i, v := range row { + if !wrapIdx[i] { + continue + } + lines := pdf.SplitLines([]byte(strings.TrimSpace(v)), widths[i]-2) + if len(lines) > maxLines { + maxLines = len(lines) + } + } + h := float64(maxLines)*lineH + 2 + if h < minH { + return minH + } + return h +} + +func drawPDFCellWrapped(pdf *gofpdf.Fpdf, value string, x, y, w, h float64, align string, lineH float64) { + text := strings.TrimSpace(value) + lines := pdf.SplitLines([]byte(text), w-2) + if len(lines) == 0 { + lines = [][]byte{[]byte("")} + } + + startY := y + (h-(float64(len(lines))*lineH))/2 + if startY < y+0.7 { + startY = y + 0.7 + } + + for _, ln := range lines { + pdf.SetXY(x+1, startY) + pdf.CellFormat(w-2, lineH, string(ln), "", 0, align, false, 0, "") + startY += lineH + } +} + func formatCurrencyMapPDF(m map[string]float64) string { if len(m) == 0 { return "-" diff --git a/svc/routes/login.go b/svc/routes/login.go index f342921..d9a340f 100644 --- a/svc/routes/login.go +++ b/svc/routes/login.go @@ -445,6 +445,12 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc { return } + payload.Code = strings.TrimSpace(payload.Code) + payload.FullName = strings.TrimSpace(payload.FullName) + payload.Email = strings.TrimSpace(payload.Email) + payload.Mobile = strings.TrimSpace(payload.Mobile) + payload.Address = strings.TrimSpace(payload.Address) + if payload.Code == "" { http.Error(w, "Kullanıcı kodu zorunludur", http.StatusUnprocessableEntity) return @@ -460,16 +466,17 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc { var newID int64 err = tx.QueryRow(` INSERT INTO mk_dfusr ( - code, + username, is_active, full_name, email, mobile, address, force_password_change, - last_updated_date + created_at, + updated_at ) - VALUES ($1,$2,$3,$4,$5,$6,true,NOW()) + VALUES ($1,$2,$3,$4,$5,$6,true,NOW(),NOW()) RETURNING id `, payload.Code, @@ -481,7 +488,7 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc { ).Scan(&newID) if err != nil { - log.Println("USER INSERT ERROR:", err) + log.Printf("USER INSERT ERROR code=%q email=%q err=%v", payload.Code, payload.Email, err) http.Error(w, "Kullanıcı oluşturulamadı", http.StatusInternalServerError) return } diff --git a/svc/routes/statement_aging_pdf.go b/svc/routes/statement_aging_pdf.go index 780d4ad..0a6cb73 100644 --- a/svc/routes/statement_aging_pdf.go +++ b/svc/routes/statement_aging_pdf.go @@ -78,6 +78,7 @@ func ExportStatementAgingPDFHandler(_ *sql.DB) http.HandlerFunc { params.CariSearch, detailed, "Cari Yaslandirmali Ekstre", + true, summaries, detailsByMaster, ) diff --git a/ui/src/pages/AccountAgingStatement.vue b/ui/src/pages/AccountAgingStatement.vue index 9b6fa95..66482c9 100644 --- a/ui/src/pages/AccountAgingStatement.vue +++ b/ui/src/pages/AccountAgingStatement.vue @@ -186,8 +186,8 @@ -