Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-03-09 13:19:14 +03:00
parent 6df18ed14d
commit 0d303f0c0f
12 changed files with 382 additions and 280 deletions

View File

@@ -101,15 +101,19 @@ func GetAccounts(ctx context.Context) ([]models.Account, error) {
return nil, err
}
if len(acc.AccountCode) >= 4 {
acc.DisplayCode =
strings.TrimSpace(acc.AccountCode[:3] + " " + acc.AccountCode[3:])
} else {
acc.DisplayCode = acc.AccountCode
}
acc.DisplayCode = formatAccountDisplayCode(acc.AccountCode)
accounts = append(accounts, acc)
}
return accounts, rows.Err()
}
func formatAccountDisplayCode(code string) string {
trimmed := strings.TrimSpace(code)
runes := []rune(trimmed)
if len(runes) <= 3 {
return trimmed
}
return strings.TrimSpace(string(runes[:3]) + " " + string(runes[3:]))
}

View File

@@ -15,15 +15,16 @@ import (
)
type mkCariBakiyeLine struct {
CurrAccTypeCode int
CariKodu string
CariDoviz string
SirketKodu int
PislemTipi string
YerelBakiye float64
Bakiye float64
VadeGun float64
VadeBelgeGun float64
CurrAccTypeCode int
CariKodu string
CariDoviz string
SirketKodu int
PislemTipi string
ParasalIslemTipi string
YerelBakiye float64
Bakiye float64
VadeGun float64
VadeBelgeGun float64
}
type cariMeta struct {
@@ -181,13 +182,13 @@ func GetCustomerBalanceList(ctx context.Context, params models.CustomerBalanceLi
}
usd := toUSD(ln.Bakiye, curr, usdTry, rateMap)
switch strings.TrimSpace(ln.PislemTipi) {
case "1_2":
add12, add13 := resolveBalanceBuckets(ln)
if add12 {
row.Bakiye12 += ln.Bakiye
row.TLBakiye12 += ln.YerelBakiye
row.USDBakiye12 += usd
case "1_3":
}
if add13 {
row.Bakiye13 += ln.Bakiye
row.TLBakiye13 += ln.YerelBakiye
row.USDBakiye13 += usd
@@ -319,13 +320,14 @@ func loadBalanceLines(ctx context.Context, selectedDate, cariSearch string) ([]m
return nil, err
}
query := fmt.Sprintf(`
queryTemplate := `
SELECT
CurrAccTypeCode,
CariKodu,
CariDoviz,
SirketKodu,
PislemTipi,
%s
YerelBakiye,
Bakiye,
CAST(0 AS DECIMAL(18,4)) AS Vade_Gun,
@@ -333,13 +335,33 @@ func loadBalanceLines(ctx context.Context, selectedDate, cariSearch string) ([]m
FROM dbo.MK_CARI_BAKIYE_LIST(@SonTarih)
WHERE (@CariSearch = '' OR CariKodu LIKE '%%' + @CariSearch + '%%')
AND %s
`, piyasaScope)
`
rows, err := db.MssqlDB.QueryContext(ctx, query,
sql.Named("SonTarih", selectedDate),
sql.Named("CariSearch", strings.TrimSpace(cariSearch)),
selectParasalCandidates := make([]string, 0, 7)
if expr := strings.TrimSpace(resolveParasalIslemSelectExpr(ctx, "SELECT * FROM dbo.MK_CARI_BAKIYE_LIST('2000-01-01')")); expr != "" {
selectParasalCandidates = append(selectParasalCandidates, expr)
}
selectParasalCandidates = append(selectParasalCandidates,
"CAST(ATAtt01 AS varchar(16)) AS ParasalIslemTipi,",
"CAST(ParasalIslemTipi AS varchar(16)) AS ParasalIslemTipi,",
"CAST(ParislemTipi AS varchar(16)) AS ParasalIslemTipi,",
"CAST(ParIslemTipi AS varchar(16)) AS ParasalIslemTipi,",
"CAST('' AS varchar(16)) AS ParasalIslemTipi,",
)
if err != nil {
var rows *sql.Rows
for i, sel := range selectParasalCandidates {
query := fmt.Sprintf(queryTemplate, sel, piyasaScope)
rows, err = db.MssqlDB.QueryContext(ctx, query,
sql.Named("SonTarih", selectedDate),
sql.Named("CariSearch", strings.TrimSpace(cariSearch)),
)
if err == nil {
break
}
if i < len(selectParasalCandidates)-1 && isInvalidColumnError(err) {
continue
}
return nil, fmt.Errorf("MK_CARI_BAKIYE_LIST query error: %w", err)
}
defer rows.Close()
@@ -353,6 +375,7 @@ func loadBalanceLines(ctx context.Context, selectedDate, cariSearch string) ([]m
&r.CariDoviz,
&r.SirketKodu,
&r.PislemTipi,
&r.ParasalIslemTipi,
&r.YerelBakiye,
&r.Bakiye,
&r.VadeGun,
@@ -648,7 +671,7 @@ func buildFilters(params models.CustomerBalanceListParams) balanceFilters {
piyasa: parseCSVSet(params.Piyasa),
temsilci: parseCSVSet(params.Temsilci),
riskDurumu: parseCSVSet(params.RiskDurumu),
islemTipi: parseCSVSet(params.IslemTipi),
islemTipi: parseIslemTipiSet(params.IslemTipi),
ulke: parseCSVSet(params.Ulke),
il: parseCSVSet(params.Il),
ilce: parseCSVSet(params.Ilce),
@@ -707,6 +730,28 @@ func parseCSVSet(v string) map[string]struct{} {
return out
}
func parseIslemTipiSet(v string) map[string]struct{} {
raw := parseCSVSet(v)
if len(raw) == 0 {
return raw
}
out := make(map[string]struct{}, 2)
for token := range raw {
switch strings.ToLower(strings.TrimSpace(token)) {
case "1_2", "prbr_1_2", "usd_1_2", "try_1_2", "tl_1_2", "usd_bakiye_1_2", "tl_bakiye_1_2":
out["1_2"] = struct{}{}
case "1_3", "prbr_1_3", "usd_1_3", "try_1_3", "tl_1_3", "usd_bakiye_1_3", "tl_bakiye_1_3":
out["1_3"] = struct{}{}
}
}
if len(out) == 0 {
return raw
}
return out
}
func getAuthorizedPiyasaCodes(ctx context.Context) ([]string, error) {
claims, ok := auth.GetClaimsFromContext(ctx)
if !ok || claims == nil {
@@ -794,3 +839,181 @@ func firstNonEmpty(v ...string) string {
}
return ""
}
func isInvalidColumnError(err error) bool {
if err == nil {
return false
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "invalid column name")
}
func shouldSkipBalanceLine(ln mkCariBakiyeLine) bool {
add12, add13 := resolveBalanceBuckets(ln)
p := strings.TrimSpace(ln.PislemTipi)
if p == "1_2" {
return !add12
}
if p == "1_3" {
return !add13
}
return false
}
func resolveBalanceBuckets(ln mkCariBakiyeLine) (add12 bool, add13 bool) {
p := strings.TrimSpace(ln.PislemTipi)
t := normalizeParasalIslemTipi(ln.ParasalIslemTipi)
switch t {
case "1":
return true, true
case "2", "1_2":
return true, false
case "3", "1_3":
return false, true
}
// Parasal tip yoksa eski davranis: PislemTipi'ne gore ayir.
if p == "1_2" {
return true, false
}
if p == "1_3" {
return false, true
}
return false, false
}
func normalizeParasalIslemTipi(v string) string {
s := strings.TrimSpace(v)
if s == "" {
return ""
}
lower := strings.ToLower(s)
compact := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(lower, " ", ""), "-", "_"), "/", "_")
if strings.Contains(compact, "1_2") {
return "1_2"
}
if strings.Contains(compact, "1_3") {
return "1_3"
}
// "1,2" / "1,3" gibi liste formatlarini dogrudan yakala.
tokenized := strings.NewReplacer(" ", "", ";", ",", "|", ",", "/", ",", "-", ",", "_", ",").Replace(lower)
parts := strings.Split(tokenized, ",")
has1 := false
has2 := false
has3 := false
for _, p := range parts {
t := strings.TrimSpace(p)
switch t {
case "1":
has1 = true
case "2":
has2 = true
case "3":
has3 = true
}
}
if has1 && has2 {
return "1_2"
}
if has1 && has3 {
return "1_3"
}
if has2 && !has1 && !has3 {
return "2"
}
if has3 && !has1 && !has2 {
return "3"
}
if has1 && !has2 && !has3 {
return "1"
}
// "2.00", "2,00", " 2 " gibi varyasyonlari tek tipe indir.
s = strings.ReplaceAll(s, ",", ".")
if n, err := strconv.ParseFloat(s, 64); err == nil {
return strconv.Itoa(int(n))
}
// Metinsel geldiyse ilk rakam bloğunu al.
start := -1
end := -1
for i, r := range s {
if r >= '0' && r <= '9' {
if start == -1 {
start = i
}
end = i
continue
}
if start != -1 {
break
}
}
if start == -1 || end < start {
return s
}
return s[start : end+1]
}
func resolveParasalIslemSelectExpr(ctx context.Context, sampleQuery string) string {
sampleQuery = strings.TrimSpace(sampleQuery)
if sampleQuery == "" {
return ""
}
metaQuery := `
SELECT name
FROM sys.dm_exec_describe_first_result_set(@tsql, NULL, 0)
WHERE error_number IS NULL
AND name IS NOT NULL
`
rows, err := db.MssqlDB.QueryContext(ctx, metaQuery, sql.Named("tsql", sampleQuery))
if err != nil {
return ""
}
defer rows.Close()
type candidate struct {
key string
expr string
}
priority := []candidate{
{key: "ata tt01", expr: "CAST(%s AS varchar(16)) AS ParasalIslemTipi,"},
{key: "atatt01", expr: "CAST(%s AS varchar(16)) AS ParasalIslemTipi,"},
{key: "parasalislemtipi", expr: "CAST(%s AS varchar(16)) AS ParasalIslemTipi,"},
{key: "parislemtipi", expr: "CAST(%s AS varchar(16)) AS ParasalIslemTipi,"},
{key: "parislemtur", expr: "CAST(%s AS varchar(16)) AS ParasalIslemTipi,"},
}
available := make(map[string]string)
for rows.Next() {
var col sql.NullString
if err := rows.Scan(&col); err != nil {
return ""
}
name := strings.TrimSpace(col.String)
if name == "" {
continue
}
normalized := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(name, "_", ""), " ", ""))
available[normalized] = name
}
if err := rows.Err(); err != nil {
return ""
}
for _, c := range priority {
key := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(c.key, "_", ""), " ", ""))
if col, ok := available[key]; ok {
return fmt.Sprintf(c.expr, quoteSQLIdent(col))
}
}
return ""
}
func quoteSQLIdent(ident string) string {
return "[" + strings.ReplaceAll(strings.TrimSpace(ident), "]", "]]") + "]"
}

View File

@@ -137,13 +137,13 @@ func GetStatementAgingBalanceList(ctx context.Context, params models.CustomerBal
usd := toUSD(ln.Bakiye, curr, usdTry, rateMap)
tl := toTRY(ln.Bakiye, curr, rateMap)
switch strings.TrimSpace(ln.PislemTipi) {
case "1_2":
add12, add13 := resolveBalanceBuckets(ln)
if add12 {
row.Bakiye12 += ln.Bakiye
row.TLBakiye12 += tl
row.USDBakiye12 += usd
case "1_3":
}
if add13 {
row.Bakiye13 += ln.Bakiye
row.TLBakiye13 += tl
row.USDBakiye13 += usd
@@ -187,13 +187,14 @@ func loadAgingBalanceLines(ctx context.Context, cariSearch string) ([]mkCariBaki
return nil, err
}
query := fmt.Sprintf(`
queryTemplate := `
SELECT
CurrAccTypeCode,
CariKodu = LTRIM(RTRIM(CariKodu)),
CariDoviz = LTRIM(RTRIM(CariDoviz)),
SirketKodu,
PislemTipi,
%s
YerelBakiye = CAST(0 AS DECIMAL(18,2)),
Bakiye,
Vade_Gun,
@@ -202,10 +203,30 @@ func loadAgingBalanceLines(ctx context.Context, cariSearch string) ([]mkCariBaki
WHERE (@CariSearch = '' OR LTRIM(RTRIM(CariKodu)) LIKE '%%' + @CariSearch + '%%')
AND %s
ORDER BY CariKodu, CariDoviz, PislemTipi
`, piyasaScope)
`
rows, err := db.MssqlDB.QueryContext(ctx, query, sql.Named("CariSearch", strings.TrimSpace(cariSearch)))
if err != nil {
selectParasalCandidates := make([]string, 0, 7)
if expr := strings.TrimSpace(resolveParasalIslemSelectExpr(ctx, "SELECT * FROM dbo.CARI_BAKIYE_GUN_CACHE")); expr != "" {
selectParasalCandidates = append(selectParasalCandidates, expr)
}
selectParasalCandidates = append(selectParasalCandidates,
"CAST(ATAtt01 AS varchar(16)) AS ParasalIslemTipi,",
"CAST(ParasalIslemTipi AS varchar(16)) AS ParasalIslemTipi,",
"CAST(ParislemTipi AS varchar(16)) AS ParasalIslemTipi,",
"CAST(ParIslemTipi AS varchar(16)) AS ParasalIslemTipi,",
"CAST('' AS varchar(16)) AS ParasalIslemTipi,",
)
var rows *sql.Rows
for i, sel := range selectParasalCandidates {
query := fmt.Sprintf(queryTemplate, sel, piyasaScope)
rows, err = db.MssqlDB.QueryContext(ctx, query, sql.Named("CariSearch", strings.TrimSpace(cariSearch)))
if err == nil {
break
}
if i < len(selectParasalCandidates)-1 && isInvalidColumnError(err) {
continue
}
return nil, fmt.Errorf("CARI_BAKIYE_GUN_CACHE query error: %w", err)
}
defer rows.Close()
@@ -219,6 +240,7 @@ func loadAgingBalanceLines(ctx context.Context, cariSearch string) ([]mkCariBaki
&r.CariDoviz,
&r.SirketKodu,
&r.PislemTipi,
&r.ParasalIslemTipi,
&r.YerelBakiye,
&r.Bakiye,
&r.VadeGun,

View File

@@ -23,7 +23,10 @@ SELECT
a.ItemCode AS Urun_Kodu,
a.ColorCode AS Urun_Rengi,
SUM(a.Qty1) AS Toplam_Adet,
SUM(ABS(a.Doc_Price)) AS Toplam_Fiyat,
CAST(
SUM(a.Qty1 * ABS(a.Doc_Price)) / NULLIF(SUM(a.Qty1), 0)
AS numeric(18,2)
) AS Toplam_Fiyat,
CAST(SUM(a.Qty1 * ABS(a.Doc_Price)) AS numeric(18,2)) AS Toplam_Tutar
FROM AllInvoicesWithAttributes a
LEFT JOIN prItemAttribute AnaGrup

View File

@@ -23,25 +23,34 @@ type ProductImageItem struct {
ContentURL string `json:"content_url"`
}
//
// LIST PRODUCT IMAGES
//
// GET /api/product-images?code=...&color=...
func GetProductImagesHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
reqID := strings.TrimSpace(r.Header.Get("X-Request-ID"))
if reqID == "" {
reqID = uuid.NewString()
}
w.Header().Set("X-Request-ID", reqID)
code := strings.TrimSpace(r.URL.Query().Get("code"))
color := strings.TrimSpace(r.URL.Query().Get("color"))
if code == "" {
slog.Warn("product_images.list.bad_request",
"req_id", reqID,
"path", r.URL.Path,
"query", r.URL.RawQuery,
"reason", "missing_code",
)
http.Error(w, "Eksik parametre: code gerekli", http.StatusBadRequest)
return
}
@@ -50,38 +59,59 @@ func GetProductImagesHandler(pg *sql.DB) http.HandlerFunc {
SELECT
b.id,
b.file_name,
COALESCE(b.file_size, 0) AS file_size,
COALESCE(b.storage_path, '') AS storage_path
COALESCE(b.file_size,0) AS file_size,
COALESCE(b.storage_path,'') AS storage_path
FROM dfblob b
JOIN mmitem i
ON i.id = b.src_id
WHERE b.typ = 'img'
AND b.src_table = 'mmitem'
AND UPPER(i.code) = UPPER($1)
AND ($2 = '' OR b.file_name ILIKE '%' || '-' || $2 || '-%')
ORDER BY COALESCE(b.sort_order, 999999), b.zlins_dttm DESC, b.id DESC
AND (
$2 = ''
OR b.file_name ILIKE '%' || '-' || $2 || '-%'
OR b.file_name ILIKE '%' || '-' || $2 || '_%'
)
ORDER BY
COALESCE(b.sort_order,999999),
b.zlins_dttm DESC,
b.id DESC
`
rows, err := pg.Query(query, code, color)
if err != nil {
slog.Error("product_images.list.query_failed",
"req_id", reqID,
"code", code,
"color", color,
"err", err.Error(),
)
http.Error(w, "Gorsel sorgu hatasi: "+err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
items := make([]ProductImageItem, 0, 16)
for rows.Next() {
var it ProductImageItem
if err := rows.Scan(&it.ID, &it.FileName, &it.FileSize, &it.Storage); err != nil {
if err := rows.Scan(
&it.ID,
&it.FileName,
&it.FileSize,
&it.Storage,
); err != nil {
continue
}
it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID)
items = append(items, it)
}
@@ -97,24 +127,35 @@ ORDER BY COALESCE(b.sort_order, 999999), b.zlins_dttm DESC, b.id DESC
}
}
//
// GET IMAGE CONTENT
//
// GET /api/product-images/{id}/content
func GetProductImageContentHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
reqID := strings.TrimSpace(r.Header.Get("X-Request-ID"))
if reqID == "" {
reqID = uuid.NewString()
}
w.Header().Set("X-Request-ID", reqID)
idStr := mux.Vars(r)["id"]
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
slog.Warn("product_images.content.bad_request",
"req_id", reqID,
"id_raw", idStr,
"path", r.URL.Path,
"reason", "invalid_id",
)
http.Error(w, "Gecersiz gorsel id", http.StatusBadRequest)
return
}
@@ -128,83 +169,89 @@ func GetProductImageContentHandler(pg *sql.DB) http.HandlerFunc {
err = pg.QueryRow(`
SELECT
COALESCE(file_name, ''),
COALESCE(storage_path, ''),
COALESCE(stored_in_db, false),
COALESCE(file_name,''),
COALESCE(storage_path,''),
COALESCE(stored_in_db,false),
bin
FROM dfblob
WHERE id = $1
AND typ = 'img'
`, id).Scan(&fileName, &storagePath, &storedInDB, &binData)
if err != nil {
if err == sql.ErrNoRows {
slog.Warn("product_images.content.not_found_row",
"req_id", reqID,
"id", id,
)
http.NotFound(w, r)
return
}
slog.Error("product_images.content.query_failed",
"req_id", reqID,
"id", id,
"err", err.Error(),
)
http.Error(w, "Gorsel okunamadi: "+err.Error(), http.StatusInternalServerError)
return
}
// DB içinde binary saklıysa
if storedInDB && len(binData) > 0 {
slog.Info("product_images.content.served_from_db",
"req_id", reqID,
"id", id,
"file_name", fileName,
"bytes", len(binData),
)
w.Header().Set("Content-Type", http.DetectContentType(binData))
w.Header().Set("Cache-Control", "public, max-age=3600")
_, _ = w.Write(binData)
return
}
resolved, tried := resolveStoragePath(storagePath)
if resolved == "" {
slog.Warn("product_images.content.file_not_found",
"req_id", reqID,
"id", id,
"stored_in_db", storedInDB,
"file_name", fileName,
"storage_path", storagePath,
"tried", tried,
)
http.NotFound(w, r)
return
}
slog.Info("product_images.content.served_from_file",
"req_id", reqID,
"id", id,
"file_name", fileName,
"storage_path", storagePath,
"resolved_path", resolved,
)
w.Header().Set("Cache-Control", "public, max-age=3600")
http.ServeFile(w, r, resolved)
}
}
//
// FILE PATH RESOLVER
//
func resolveStoragePath(storagePath string) (string, []string) {
raw := strings.TrimSpace(storagePath)
if raw == "" {
return "", nil
}
// URL/query temizligi ve platforma uygun normalize
if i := strings.Index(raw, "?"); i >= 0 {
raw = raw[:i]
}
raw = strings.ReplaceAll(raw, "\\", "/")
if scheme := strings.Index(raw, "://"); scheme >= 0 {
rest := raw[scheme+3:]
if i := strings.Index(rest, "/"); i >= 0 {
@@ -215,47 +262,36 @@ func resolveStoragePath(storagePath string) (string, []string) {
raw = strings.TrimPrefix(raw, "./")
raw = strings.TrimPrefix(raw, "/")
raw = strings.TrimPrefix(raw, "uploads/")
raw = filepath.ToSlash(filepath.Clean(raw))
relUploads := filepath.FromSlash(filepath.Join("uploads", raw))
rawT300 := raw
relUploadsT300 := relUploads
if strings.Contains(filepath.ToSlash(relUploads), "uploads/image/") &&
!strings.Contains(filepath.ToSlash(relUploads), "uploads/image/t300/") {
rawT300 = strings.Replace(filepath.ToSlash(raw), "image/", "image/t300/", 1)
relUploadsT300 = filepath.FromSlash(
strings.Replace(filepath.ToSlash(relUploads), "uploads/image/", "uploads/image/t300/", 1),
)
}
candidates := []string{
filepath.Clean(storagePath),
filepath.FromSlash(filepath.Clean(strings.TrimPrefix(storagePath, "/"))),
filepath.FromSlash(filepath.Clean(raw)),
relUploads,
relUploadsT300,
filepath.Join(".", relUploads),
filepath.Join(".", relUploadsT300),
filepath.Join("..", relUploads),
filepath.Join("..", relUploadsT300),
filepath.Join("..", "..", relUploads),
filepath.Join("..", "..", relUploadsT300),
}
if root := strings.TrimSpace(os.Getenv("BLOB_ROOT")); root != "" {
candidates = append(candidates,
filepath.Join(root, raw),
filepath.Join(root, filepath.FromSlash(rawT300)),
filepath.Join(root, relUploads),
filepath.Join(root, relUploadsT300),
filepath.Join(root, "uploads", raw),
filepath.Join(root, "uploads", filepath.FromSlash(rawT300)),
)
}
for _, p := range candidates {
if p == "" {
continue
}
if st, err := os.Stat(p); err == nil && !st.IsDir() {
return p, candidates
}