From ce31aff64549d51531e8ee00503ac68fce421092 Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Tue, 3 Mar 2026 10:16:05 +0300 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- .../setup_statement_aging_cache_pipeline.sql | 18 + svc/main.go | 12 + svc/models/customer_balance_list.go | 2 + svc/queries/customer_balance_list.go | 8 +- svc/queries/statement_aging.go | 59 +- svc/queries/statement_aging_balance_list.go | 244 +++ svc/queries/statement_aging_cache.go | 20 + svc/routes/customer_balance_excel.go | 29 +- svc/routes/customer_balance_pdf.go | 86 +- svc/routes/orderproductionitems.go | 5 +- svc/routes/statement_aging.go | 26 +- svc/routes/statement_aging_cache.go | 34 + svc/routes/statement_aging_excel.go | 90 ++ svc/routes/statement_aging_pdf.go | 44 +- ui/.quasar/prod-spa/app.js | 75 - ui/.quasar/prod-spa/client-entry.js | 154 -- ui/.quasar/prod-spa/client-prefetch.js | 116 -- ui/.quasar/prod-spa/quasar-user-options.js | 23 - ...g.js.temporary.compiled.1772520846397.mjs} | 0 ui/src/css/app.css | 8 + ui/src/layouts/MainLayout.vue | 20 +- ui/src/pages/AccountAgingStatement.vue | 1392 +++++++++++------ ui/src/pages/CustomerBalanceList.vue | 45 +- ui/src/pages/OrderProductionUpdate.vue | 203 ++- ui/src/router/routes.js | 2 +- ui/src/stores/accountAgingBalanceStore.js | 263 ++++ 26 files changed, 2013 insertions(+), 965 deletions(-) create mode 100644 scripts/sql/setup_statement_aging_cache_pipeline.sql create mode 100644 svc/queries/statement_aging_balance_list.go create mode 100644 svc/queries/statement_aging_cache.go create mode 100644 svc/routes/statement_aging_cache.go create mode 100644 svc/routes/statement_aging_excel.go delete mode 100644 ui/.quasar/prod-spa/app.js delete mode 100644 ui/.quasar/prod-spa/client-entry.js delete mode 100644 ui/.quasar/prod-spa/client-prefetch.js delete mode 100644 ui/.quasar/prod-spa/quasar-user-options.js rename ui/{quasar.config.js.temporary.compiled.1772488385975.mjs => quasar.config.js.temporary.compiled.1772520846397.mjs} (100%) create mode 100644 ui/src/stores/accountAgingBalanceStore.js diff --git a/scripts/sql/setup_statement_aging_cache_pipeline.sql b/scripts/sql/setup_statement_aging_cache_pipeline.sql new file mode 100644 index 0000000..07808c6 --- /dev/null +++ b/scripts/sql/setup_statement_aging_cache_pipeline.sql @@ -0,0 +1,18 @@ +USE [BAGGI_V3] +GO + +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO + +CREATE OR ALTER PROCEDURE [dbo].[SP_BUILD_STATEMENT_AGING_PIPELINE] +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + + EXEC dbo.SP_BUILD_CARI_VADE_GUN_STAGING; + EXEC dbo.SP_BUILD_CARI_BAKIYE_CACHE; +END +GO diff --git a/svc/main.go b/svc/main.go index 593a191..f99ad74 100644 --- a/svc/main.go +++ b/svc/main.go @@ -449,12 +449,24 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router wrapV3(http.HandlerFunc(routes.GetStatementAgingHandler)), ) + bindV3(r, pgDB, + "/api/finance/account-aging-statement/rebuild-cache", "POST", + "finance", "update", + wrapV3(http.HandlerFunc(routes.RebuildStatementAgingCacheHandler)), + ) + bindV3(r, pgDB, "/api/finance/account-aging-statement/export-pdf", "GET", "finance", "export", wrapV3(routes.ExportStatementAgingPDFHandler(mssql)), ) + bindV3(r, pgDB, + "/api/finance/account-aging-statement/export-excel", "GET", + "finance", "export", + wrapV3(routes.ExportStatementAgingExcelHandler(mssql)), + ) + // ============================================================ // REPORT (STATEMENTS) // ============================================================ diff --git a/svc/models/customer_balance_list.go b/svc/models/customer_balance_list.go index f61ef4b..baff6e5 100644 --- a/svc/models/customer_balance_list.go +++ b/svc/models/customer_balance_list.go @@ -41,4 +41,6 @@ type CustomerBalanceListRow struct { Bakiye13 float64 `json:"bakiye_1_3"` TLBakiye13 float64 `json:"tl_bakiye_1_3"` USDBakiye13 float64 `json:"usd_bakiye_1_3"` + VadeGun float64 `json:"vade_gun"` + VadeBelgeGun float64 `json:"vade_belge_tarihi_gun"` } diff --git a/svc/queries/customer_balance_list.go b/svc/queries/customer_balance_list.go index 82433f8..339b44d 100644 --- a/svc/queries/customer_balance_list.go +++ b/svc/queries/customer_balance_list.go @@ -22,6 +22,8 @@ type mkCariBakiyeLine struct { PislemTipi string YerelBakiye float64 Bakiye float64 + VadeGun float64 + VadeBelgeGun float64 } type cariMeta struct { @@ -320,7 +322,9 @@ func loadBalanceLines(ctx context.Context, selectedDate, cariSearch string) ([]m SirketKodu, PislemTipi, YerelBakiye, - Bakiye + Bakiye, + CAST(0 AS DECIMAL(18,4)) AS Vade_Gun, + CAST(0 AS DECIMAL(18,4)) AS Vade_BelgeTarihi_Gun FROM dbo.MK_CARI_BAKIYE_LIST(@SonTarih) WHERE (@CariSearch = '' OR CariKodu LIKE '%' + @CariSearch + '%') ` @@ -345,6 +349,8 @@ func loadBalanceLines(ctx context.Context, selectedDate, cariSearch string) ([]m &r.PislemTipi, &r.YerelBakiye, &r.Bakiye, + &r.VadeGun, + &r.VadeBelgeGun, ); err != nil { return nil, err } diff --git a/svc/queries/statement_aging.go b/svc/queries/statement_aging.go index 83c33fe..210df28 100644 --- a/svc/queries/statement_aging.go +++ b/svc/queries/statement_aging.go @@ -15,30 +15,59 @@ import ( func GetStatementAging(params models.StatementAgingParams) ([]map[string]interface{}, error) { accountCode := normalizeMasterAccountCode(params.AccountCode) - if strings.TrimSpace(accountCode) == "" { - return nil, fmt.Errorf("accountcode is required") - } - if strings.TrimSpace(params.EndDate) == "" { - return nil, fmt.Errorf("enddate is required") - } useType2, useType3 := resolveUseTypes(params.Parislemler) - endDate, _ := time.Parse("2006-01-02", strings.TrimSpace(params.EndDate)) + endDateText := strings.TrimSpace(params.EndDate) + if endDateText == "" { + endDateText = time.Now().Format("2006-01-02") + } + endDate, _ := time.Parse("2006-01-02", endDateText) + + cariFilter := "" + if strings.TrimSpace(accountCode) != "" { + cariFilter = strings.TrimSpace(accountCode) + } rows, err := db.MssqlDB.Query(` - EXEC dbo.SP_FIFO_MATCH_FINAL - @Cari8 = @Cari8, - @SonTarih = @SonTarih, - @UseType2 = @UseType2, - @UseType3 = @UseType3; + SELECT TOP (100) + Cari8 = LEFT(LTRIM(RTRIM(CariKodu)), 8), + CariDetay = LTRIM(RTRIM(CariKodu)), + FaturaCari = LTRIM(RTRIM(CariKodu)), + OdemeCari = LTRIM(RTRIM(CariKodu)), + FaturaRef = CAST(NULL AS NVARCHAR(50)), + OdemeRef = CAST(NULL AS NVARCHAR(50)), + FaturaTarihi = CAST(NULL AS DATE), + OdemeTarihi = CAST(NULL AS DATE), + OdemeDocDate = CAST(NULL AS DATE), + EslesenTutar = CAST(Bakiye AS DECIMAL(18,2)), + GunSayisi = CAST(Vade_Gun AS DECIMAL(18,2)), + GunSayisi_DocDate = CAST(Vade_BelgeTarihi_Gun AS DECIMAL(18,2)), + Aciklama = CAST('AcikKalem' AS NVARCHAR(30)), + DocCurrencyCode = LTRIM(RTRIM(CariDoviz)), + PislemTipi, + SirketKodu, + CurrAccTypeCode, + Bakiye, + Vade_Gun, + Vade_BelgeTarihi_Gun, + SonTarih, + HesaplamaTarihi + FROM dbo.CARI_BAKIYE_GUN_CACHE + WHERE + ( + (@UseType2 = 1 AND PislemTipi = '1_2') + OR + (@UseType3 = 1 AND PislemTipi = '1_3') + ) + AND (@CariFilter = '' OR LTRIM(RTRIM(CariKodu)) LIKE @CariFilter + '%') + ORDER BY CariKodu, CariDoviz, PislemTipi; `, - sql.Named("Cari8", accountCode), - sql.Named("SonTarih", params.EndDate), sql.Named("UseType2", useType2), sql.Named("UseType3", useType3), + sql.Named("CariFilter", cariFilter), ) if err != nil { - return nil, fmt.Errorf("SP_FIFO_MATCH_FINAL query error: %w", err) + return nil, fmt.Errorf("CARI_BAKIYE_GUN_CACHE query error: %w", err) } defer rows.Close() diff --git a/svc/queries/statement_aging_balance_list.go b/svc/queries/statement_aging_balance_list.go new file mode 100644 index 0000000..ddb9f5f --- /dev/null +++ b/svc/queries/statement_aging_balance_list.go @@ -0,0 +1,244 @@ +package queries + +import ( + "bssapp-backend/models" + "context" + "database/sql" + "fmt" + "log" + "math" + "sort" + "strconv" + "strings" + "time" + + "bssapp-backend/db" +) + +func GetStatementAgingBalanceList(ctx context.Context, params models.CustomerBalanceListParams) ([]models.CustomerBalanceListRow, error) { + selectedDate := strings.TrimSpace(params.SelectedDate) + if selectedDate == "" { + selectedDate = time.Now().Format("2006-01-02") + } + + lines, err := loadAgingBalanceLines(ctx, strings.TrimSpace(params.CariSearch)) + if err != nil { + return nil, err + } + + metaMap, err := loadCariMetaMap(ctx, lines) + if err != nil { + log.Printf("statement_aging_balance: cari meta query failed, fallback without meta: %v", err) + metaMap = map[string]cariMeta{} + } + + masterMetaMap, err := loadMasterCariMetaMap(ctx, lines) + if err != nil { + log.Printf("statement_aging_balance: master cari meta query failed, fallback without master meta: %v", err) + masterMetaMap = map[string]masterCariMeta{} + } + + companyMap, err := loadCompanyMap(ctx) + if err != nil { + return nil, err + } + + glMap, err := loadGLAccountMap(ctx, lines) + if err != nil { + return nil, err + } + + rateMap, err := loadNearestTryRates(ctx) + if err != nil { + return nil, err + } + usdTry := rateMap["USD"] + if usdTry <= 0 { + usdTry = 1 + } + + filters := buildFilters(params) + agg := make(map[string]*models.CustomerBalanceListRow, len(lines)) + weightMap := make(map[string]float64, len(lines)) + vadeSumMap := make(map[string]float64, len(lines)) + vadeBelgeSumMap := make(map[string]float64, len(lines)) + + for _, ln := range lines { + cari := strings.TrimSpace(ln.CariKodu) + if cari == "" { + continue + } + + curr := strings.ToUpper(strings.TrimSpace(ln.CariDoviz)) + if curr == "" { + curr = "TRY" + } + + meta := metaMap[metaKey(ln.CurrAccTypeCode, cari)] + meta.MuhasebeKodu = glMap[glKey(ln.CurrAccTypeCode, cari, ln.SirketKodu)] + meta.SirketDetay = companyMap[ln.SirketKodu] + master := deriveMasterCari(cari) + mm := masterMetaMap[master] + + if strings.TrimSpace(mm.Kanal1) != "" { + meta.Kanal1 = mm.Kanal1 + } + if strings.TrimSpace(mm.Piyasa) != "" { + meta.Piyasa = mm.Piyasa + } + if strings.TrimSpace(mm.Temsilci) != "" { + meta.Temsilci = mm.Temsilci + } + if strings.TrimSpace(mm.Ulke) != "" { + meta.Ulke = mm.Ulke + } + if strings.TrimSpace(mm.Il) != "" { + meta.Il = mm.Il + } + if strings.TrimSpace(mm.Ilce) != "" { + meta.Ilce = mm.Ilce + } + if strings.TrimSpace(mm.RiskDurumu) != "" { + meta.RiskDurumu = mm.RiskDurumu + } + + if !filters.matchLine(ln.PislemTipi, meta) { + continue + } + + key := strconv.Itoa(ln.CurrAccTypeCode) + "|" + cari + "|" + curr + "|" + strconv.Itoa(ln.SirketKodu) + row, ok := agg[key] + if !ok { + row = &models.CustomerBalanceListRow{ + CariIlkGrup: meta.Kanal1, + Piyasa: meta.Piyasa, + Temsilci: meta.Temsilci, + Sirket: strconv.Itoa(ln.SirketKodu), + AnaCariKodu: master, + AnaCariAdi: firstNonEmpty(mm.CariDetay, meta.CariDetay), + CariKodu: cari, + CariDetay: meta.CariDetay, + CariTip: meta.CariTip, + Kanal1: meta.Kanal1, + Ozellik03: meta.RiskDurumu, + Ozellik05: meta.Ulke, + Ozellik06: meta.Il, + Ozellik07: meta.Ilce, + Il: meta.Il, + Ilce: meta.Ilce, + MuhasebeKodu: meta.MuhasebeKodu, + TC: meta.TC, + RiskDurumu: meta.RiskDurumu, + SirketDetay: meta.SirketDetay, + CariDoviz: curr, + } + agg[key] = row + } + + usd := toUSD(ln.Bakiye, curr, usdTry, rateMap) + tl := toTRY(ln.Bakiye, curr, rateMap) + + switch strings.TrimSpace(ln.PislemTipi) { + case "1_2": + row.Bakiye12 += ln.Bakiye + row.TLBakiye12 += tl + row.USDBakiye12 += usd + case "1_3": + row.Bakiye13 += ln.Bakiye + row.TLBakiye13 += tl + row.USDBakiye13 += usd + } + + w := math.Abs(ln.Bakiye) + if w > 0 { + weightMap[key] += w + vadeSumMap[key] += (ln.VadeGun * w) + vadeBelgeSumMap[key] += (ln.VadeBelgeGun * w) + } + } + + out := make([]models.CustomerBalanceListRow, 0, len(agg)) + for k, v := range agg { + base := weightMap[k] + if base > 0 { + v.VadeGun = vadeSumMap[k] / base + v.VadeBelgeGun = vadeBelgeSumMap[k] / base + } + out = append(out, *v) + } + + sort.Slice(out, func(i, j int) bool { + if out[i].AnaCariKodu == out[j].AnaCariKodu { + if out[i].CariKodu == out[j].CariKodu { + return out[i].CariDoviz < out[j].CariDoviz + } + return out[i].CariKodu < out[j].CariKodu + } + return out[i].AnaCariKodu < out[j].AnaCariKodu + }) + + _ = selectedDate + return out, nil +} + +func loadAgingBalanceLines(ctx context.Context, cariSearch string) ([]mkCariBakiyeLine, error) { + query := ` + SELECT + CurrAccTypeCode, + CariKodu = LTRIM(RTRIM(CariKodu)), + CariDoviz = LTRIM(RTRIM(CariDoviz)), + SirketKodu, + PislemTipi, + YerelBakiye = CAST(0 AS DECIMAL(18,2)), + Bakiye, + Vade_Gun, + Vade_BelgeTarihi_Gun + FROM dbo.CARI_BAKIYE_GUN_CACHE + WHERE (@CariSearch = '' OR LTRIM(RTRIM(CariKodu)) LIKE '%' + @CariSearch + '%') + ORDER BY CariKodu, CariDoviz, PislemTipi + ` + + rows, err := db.MssqlDB.QueryContext(ctx, query, sql.Named("CariSearch", strings.TrimSpace(cariSearch))) + if err != nil { + return nil, fmt.Errorf("CARI_BAKIYE_GUN_CACHE query error: %w", err) + } + defer rows.Close() + + out := make([]mkCariBakiyeLine, 0, 4096) + for rows.Next() { + var r mkCariBakiyeLine + if err := rows.Scan( + &r.CurrAccTypeCode, + &r.CariKodu, + &r.CariDoviz, + &r.SirketKodu, + &r.PislemTipi, + &r.YerelBakiye, + &r.Bakiye, + &r.VadeGun, + &r.VadeBelgeGun, + ); err != nil { + return nil, err + } + out = append(out, r) + } + if err := rows.Err(); err != nil { + return nil, err + } + return out, nil +} + +func toTRY(amount float64, currency string, rateMap map[string]float64) float64 { + switch currency { + case "TRY": + return amount + case "": + return amount + default: + currTry := rateMap[currency] + if currTry <= 0 { + return 0 + } + return amount * currTry + } +} diff --git a/svc/queries/statement_aging_cache.go b/svc/queries/statement_aging_cache.go new file mode 100644 index 0000000..50fc8d8 --- /dev/null +++ b/svc/queries/statement_aging_cache.go @@ -0,0 +1,20 @@ +package queries + +import ( + "bssapp-backend/db" + "context" + "fmt" +) + +// RebuildStatementAgingCache runs only step 2 + step 3. +func RebuildStatementAgingCache(ctx context.Context) error { + if _, err := db.MssqlDB.ExecContext(ctx, `EXEC dbo.SP_BUILD_CARI_VADE_GUN_STAGING;`); err != nil { + return fmt.Errorf("SP_BUILD_CARI_VADE_GUN_STAGING error: %w", err) + } + + if _, err := db.MssqlDB.ExecContext(ctx, `EXEC dbo.SP_BUILD_CARI_BAKIYE_CACHE;`); err != nil { + return fmt.Errorf("SP_BUILD_CARI_BAKIYE_CACHE error: %w", err) + } + + return nil +} diff --git a/svc/routes/customer_balance_excel.go b/svc/routes/customer_balance_excel.go index 2a3b5e6..983d9ea 100644 --- a/svc/routes/customer_balance_excel.go +++ b/svc/routes/customer_balance_excel.go @@ -67,6 +67,8 @@ func ExportCustomerBalanceExcelHandler(_ *sql.DB) http.HandlerFunc { "1_2 TRY Bakiye", "1_3 USD Bakiye", "1_3 TRY Bakiye", + "Vade Gun", + "Belge Tarihi Gun", } for i, h := range headers { @@ -75,6 +77,7 @@ func ExportCustomerBalanceExcelHandler(_ *sql.DB) http.HandlerFunc { } var totalUSD12, totalTRY12, totalUSD13, totalTRY13 float64 + var totalVadeBase, totalVadeSum, totalVadeBelgeSum float64 totalPrBr12 := map[string]float64{} totalPrBr13 := map[string]float64{} @@ -83,6 +86,12 @@ func ExportCustomerBalanceExcelHandler(_ *sql.DB) http.HandlerFunc { totalTRY12 += s.TLBakiye12 totalUSD13 += s.USDBakiye13 totalTRY13 += s.TLBakiye13 + w := absFloatExcel(s.USDBakiye12) + absFloatExcel(s.TLBakiye12) + absFloatExcel(s.USDBakiye13) + absFloatExcel(s.TLBakiye13) + if w > 0 { + totalVadeBase += w + totalVadeSum += s.VadeGun * w + totalVadeBelgeSum += s.VadeBelge * w + } for k, v := range s.Bakiye12Map { totalPrBr12[k] += v } @@ -91,6 +100,13 @@ func ExportCustomerBalanceExcelHandler(_ *sql.DB) http.HandlerFunc { } } + totalVade := 0.0 + totalVadeBelge := 0.0 + if totalVadeBase > 0 { + totalVade = totalVadeSum / totalVadeBase + totalVadeBelge = totalVadeBelgeSum / totalVadeBase + } + f.SetSheetRow(sheet, "A2", &[]any{ "TOPLAM", "", @@ -103,6 +119,8 @@ func ExportCustomerBalanceExcelHandler(_ *sql.DB) http.HandlerFunc { totalTRY12, totalUSD13, totalTRY13, + totalVade, + totalVadeBelge, }) rowNo := 3 @@ -119,6 +137,8 @@ func ExportCustomerBalanceExcelHandler(_ *sql.DB) http.HandlerFunc { s.TLBakiye12, s.USDBakiye13, s.TLBakiye13, + s.VadeGun, + s.VadeBelge, }) rowNo++ } @@ -127,7 +147,7 @@ func ExportCustomerBalanceExcelHandler(_ *sql.DB) http.HandlerFunc { _ = f.SetColWidth(sheet, "B", "B", 34) _ = f.SetColWidth(sheet, "C", "E", 18) _ = f.SetColWidth(sheet, "F", "G", 34) - _ = f.SetColWidth(sheet, "H", "K", 18) + _ = f.SetColWidth(sheet, "H", "M", 18) buf, err := f.WriteToBuffer() if err != nil { @@ -143,3 +163,10 @@ func ExportCustomerBalanceExcelHandler(_ *sql.DB) http.HandlerFunc { _, _ = w.Write(buf.Bytes()) } } + +func absFloatExcel(v float64) float64 { + if v < 0 { + return -v + } + return v +} diff --git a/svc/routes/customer_balance_pdf.go b/svc/routes/customer_balance_pdf.go index 435ada7..a1ff7dd 100644 --- a/svc/routes/customer_balance_pdf.go +++ b/svc/routes/customer_balance_pdf.go @@ -29,6 +29,9 @@ type balanceSummaryPDF struct { TLBakiye12 float64 USDBakiye13 float64 TLBakiye13 float64 + VadeGun float64 + VadeBelge float64 + VadeBase float64 } func ExportCustomerBalancePDFHandler(_ *sql.DB) http.HandlerFunc { @@ -182,6 +185,12 @@ func buildCustomerBalancePDFData(rows []models.CustomerBalanceListRow) ([]balanc s.TLBakiye12 += row.TLBakiye12 s.USDBakiye13 += row.USDBakiye13 s.TLBakiye13 += row.TLBakiye13 + w := absFloat(row.Bakiye12) + absFloat(row.Bakiye13) + if w > 0 { + s.VadeBase += w + s.VadeGun += row.VadeGun * w + s.VadeBelge += row.VadeBelgeGun * w + } detailsByMaster[master] = append(detailsByMaster[master], row) } @@ -194,6 +203,11 @@ func buildCustomerBalancePDFData(rows []models.CustomerBalanceListRow) ([]balanc summaries := make([]balanceSummaryPDF, 0, len(masters)) for _, m := range masters { + s := summaryMap[m] + if s != nil && s.VadeBase > 0 { + s.VadeGun = s.VadeGun / s.VadeBase + s.VadeBelge = s.VadeBelge / s.VadeBase + } summaries = append(summaries, *summaryMap[m]) d := detailsByMaster[m] sort.SliceStable(d, func(i, j int) bool { @@ -225,11 +239,11 @@ func drawCustomerBalancePDF( 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, 52, 16, 20, 14, 24, 24, 14, 14, 14, 14}, tableW) + summaryCols := []string{"Ana Cari Kod", "Ana Cari Detay", "Piyasa", "Temsilci", "Risk", "1_2 Pr.Br", "1_3 Pr.Br", "1_2 USD", "1_2 TRY", "1_3 USD", "1_3 TRY", "Vade Gun", "Belge Gun"} + summaryW := normalizeWidths([]float64{18, 46, 14, 18, 12, 20, 20, 12, 12, 12, 12, 10, 10}, 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) + detailCols := []string{"Cari Kod", "Cari Detay", "Sirket", "Muhasebe", "Doviz", "1_2 Pr.Br", "1_3 Pr.Br", "1_2 USD", "1_2 TRY", "1_3 USD", "1_3 TRY", "Vade Gun", "Belge Gun"} + detailW := normalizeWidths([]float64{22, 40, 9, 16, 8, 20, 20, 12, 12, 12, 12, 9, 9}, tableW) header := func() { pdf.AddPage() @@ -352,18 +366,27 @@ func drawCustomerBalancePDF( formatMoneyPDF(s.TLBakiye12), formatMoneyPDF(s.USDBakiye13), formatMoneyPDF(s.TLBakiye13), + formatMoneyPDF(s.VadeGun), + formatMoneyPDF(s.VadeBelge), } wrapCols := map[int]bool{1: true, 3: true} rowH := calcWrappedRowHeight(row, summaryW, wrapCols, 3.2, 6.2) if needPage(rowH) { header() drawSummaryHeader() + pdf.SetFont("dejavu", "", 7.2) + pdf.SetTextColor(20, 20, 20) } y := pdf.GetY() x := marginL for i, v := range row { - pdf.Rect(x, y, summaryW[i], rowH, "") + if detailed { + pdf.SetFillColor(246, 241, 231) + pdf.Rect(x, y, summaryW[i], rowH, "FD") + } else { + pdf.Rect(x, y, summaryW[i], rowH, "") + } align := "L" if i >= 7 { align = "R" @@ -414,7 +437,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), + formatMoneyPDF(r.VadeGun), + formatMoneyPDF(r.VadeBelgeGun), + } + detailWrapCols := map[int]bool{1: true} + rowH := calcWrappedRowHeight(line, detailW, detailWrapCols, 3.0, 5.8) + + if needPage(rowH) { header() pdf.SetFont("dejavu", "B", 8) pdf.SetFillColor(218, 193, 151) @@ -429,33 +470,23 @@ 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 detailWrapCols[i] { + drawWrapped(v, rowX, rowY, detailW[i], rowH, 3.0, "L") + } else { + pdf.SetXY(rowX+1, rowY+(rowH-4.0)/2) + pdf.CellFormat(detailW[i]-2, 4.0, v, "", 0, align, false, 0, "") + } rowX += detailW[i] } - pdf.SetY(rowY + 5.8) + pdf.SetY(rowY + rowH) } pdf.Ln(1.2) } @@ -508,3 +539,10 @@ func formatMoneyPDF(v float64) string { return sign + strings.Join(out, ".") + "," + decPart } + +func absFloat(v float64) float64 { + if v < 0 { + return -v + } + return v +} diff --git a/svc/routes/orderproductionitems.go b/svc/routes/orderproductionitems.go index 7886dee..53f4ff2 100644 --- a/svc/routes/orderproductionitems.go +++ b/svc/routes/orderproductionitems.go @@ -249,7 +249,7 @@ func buildMissingVariants(mssql *sql.DB, orderHeaderID string, lines []models.Or newColor := strings.TrimSpace(line.NewColor) newDim2 := strings.TrimSpace(line.NewDim2) - if lineID == "" || newItem == "" || newColor == "" { + if lineID == "" || newItem == "" { continue } @@ -286,9 +286,6 @@ func validateUpdateLines(lines []models.OrderProductionUpdateLine) error { if strings.TrimSpace(line.NewItemCode) == "" { return errors.New("Yeni urun kodu zorunlu") } - if strings.TrimSpace(line.NewColor) == "" { - return errors.New("Yeni renk kodu zorunlu") - } } return nil } diff --git a/svc/routes/statement_aging.go b/svc/routes/statement_aging.go index 0d753d9..9a3a04d 100644 --- a/svc/routes/statement_aging.go +++ b/svc/routes/statement_aging.go @@ -7,6 +7,7 @@ import ( "encoding/json" "net/http" "strings" + "time" ) // GET /api/finance/account-aging-statement @@ -17,18 +18,35 @@ func GetStatementAgingHandler(w http.ResponseWriter, r *http.Request) { return } + selectedDate := time.Now().Format("2006-01-02") params := models.StatementAgingParams{ AccountCode: strings.TrimSpace(r.URL.Query().Get("accountcode")), - EndDate: strings.TrimSpace(r.URL.Query().Get("enddate")), + EndDate: selectedDate, Parislemler: r.URL.Query()["parislemler"], } - if params.AccountCode == "" || params.EndDate == "" { - http.Error(w, "accountcode and enddate are required", http.StatusBadRequest) + listParams := 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")), + Il: strings.TrimSpace(r.URL.Query().Get("il")), + Ilce: strings.TrimSpace(r.URL.Query().Get("ilce")), + } + if params.AccountCode != "" { + listParams.CariSearch = params.AccountCode + } + + if err := queries.RebuildStatementAgingCache(r.Context()); err != nil { + http.Error(w, "Error rebuilding aging cache: "+err.Error(), http.StatusInternalServerError) return } - rows, err := queries.GetStatementAging(params) + rows, err := queries.GetStatementAgingBalanceList(r.Context(), listParams) if err != nil { http.Error(w, "Error fetching aging statement: "+err.Error(), http.StatusInternalServerError) return diff --git a/svc/routes/statement_aging_cache.go b/svc/routes/statement_aging_cache.go new file mode 100644 index 0000000..eaed526 --- /dev/null +++ b/svc/routes/statement_aging_cache.go @@ -0,0 +1,34 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/queries" + "encoding/json" + "net/http" +) + +type agingCacheRefreshResponse struct { + OK bool `json:"ok"` + Message string `json:"message"` +} + +// POST /api/finance/account-aging-statement/rebuild-cache +// Runs only step2 + step3. +func RebuildStatementAgingCacheHandler(w http.ResponseWriter, r *http.Request) { + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + if err := queries.RebuildStatementAgingCache(r.Context()); err != nil { + http.Error(w, "cache rebuild error: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _ = json.NewEncoder(w).Encode(agingCacheRefreshResponse{ + OK: true, + Message: "SP_BUILD_CARI_VADE_GUN_STAGING -> SP_BUILD_CARI_BAKIYE_CACHE çalıştırıldı.", + }) +} diff --git a/svc/routes/statement_aging_excel.go b/svc/routes/statement_aging_excel.go new file mode 100644 index 0000000..350edd9 --- /dev/null +++ b/svc/routes/statement_aging_excel.go @@ -0,0 +1,90 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/models" + "bssapp-backend/queries" + "database/sql" + "fmt" + "net/http" + "strings" + "time" + + "github.com/xuri/excelize/v2" +) + +func ExportStatementAgingExcelHandler(_ *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + 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")), + Il: strings.TrimSpace(r.URL.Query().Get("il")), + Ilce: strings.TrimSpace(r.URL.Query().Get("ilce")), + } + excludeZero12 := parseBoolQuery(r.URL.Query().Get("exclude_zero_12")) + excludeZero13 := parseBoolQuery(r.URL.Query().Get("exclude_zero_13")) + + rows, err := queries.GetStatementAgingBalanceList(r.Context(), params) + if err != nil { + http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError) + return + } + rows = filterCustomerBalanceRowsForPDF(rows, excludeZero12, excludeZero13) + summaries, _ := buildCustomerBalancePDFData(rows) + + f := excelize.NewFile() + sheet := "CariYaslandirma" + f.SetSheetName("Sheet1", sheet) + + headers := []string{ + "Ana Cari Kodu", "Ana Cari Detay", "Piyasa", "Temsilci", "Risk Durumu", + "1_2 Bakiye Pr.Br", "1_3 Bakiye Pr.Br", "1_2 USD Bakiye", "1_2 TRY Bakiye", + "1_3 USD Bakiye", "1_3 TRY Bakiye", "Vade Gun", "Belge Tarihi Gun", + } + for i, h := range headers { + cell, _ := excelize.CoordinatesToCellName(i+1, 1) + f.SetCellValue(sheet, cell, h) + } + + rowNo := 2 + for _, s := range summaries { + f.SetSheetRow(sheet, fmt.Sprintf("A%d", rowNo), &[]any{ + s.AnaCariKodu, s.AnaCariAdi, s.Piyasa, s.Temsilci, s.RiskDurumu, + formatCurrencyMapPDF(s.Bakiye12Map), formatCurrencyMapPDF(s.Bakiye13Map), + s.USDBakiye12, s.TLBakiye12, s.USDBakiye13, s.TLBakiye13, s.VadeGun, s.VadeBelge, + }) + rowNo++ + } + + _ = f.SetColWidth(sheet, "A", "A", 16) + _ = f.SetColWidth(sheet, "B", "B", 34) + _ = f.SetColWidth(sheet, "C", "F", 18) + _ = f.SetColWidth(sheet, "G", "M", 18) + + buf, err := f.WriteToBuffer() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + filename := fmt.Sprintf("cari_yaslandirmali_bakiye_%s.xlsx", time.Now().Format("20060102_150405")) + w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"") + w.Header().Set("Content-Length", fmt.Sprint(len(buf.Bytes()))) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(buf.Bytes()) + } +} diff --git a/svc/routes/statement_aging_pdf.go b/svc/routes/statement_aging_pdf.go index 217139d..f430275 100644 --- a/svc/routes/statement_aging_pdf.go +++ b/svc/routes/statement_aging_pdf.go @@ -82,23 +82,30 @@ func ExportStatementAgingPDFHandler(_ *sql.DB) http.HandlerFunc { return } - params := models.StatementAgingParams{ - AccountCode: strings.TrimSpace(r.URL.Query().Get("accountcode")), - EndDate: strings.TrimSpace(r.URL.Query().Get("enddate")), - Parislemler: r.URL.Query()["parislemler"], - } - if params.AccountCode == "" || params.EndDate == "" { - http.Error(w, "accountcode and enddate are required", http.StatusBadRequest) - return + selectedDate := time.Now().Format("2006-01-02") + listParams := 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")), + Il: strings.TrimSpace(r.URL.Query().Get("il")), + Ilce: strings.TrimSpace(r.URL.Query().Get("ilce")), } + detailed := parseBoolQuery(r.URL.Query().Get("detailed")) + excludeZero12 := parseBoolQuery(r.URL.Query().Get("exclude_zero_12")) + excludeZero13 := parseBoolQuery(r.URL.Query().Get("exclude_zero_13")) - rows, err := queries.GetStatementAging(params) + rows, err := queries.GetStatementAgingBalanceList(r.Context(), listParams) if err != nil { http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError) return } - - masters := buildAgingPDFData(rows) + rows = filterCustomerBalanceRowsForPDF(rows, excludeZero12, excludeZero13) + summaries, detailsByMaster := buildCustomerBalancePDFData(rows) pdf := gofpdf.New("L", "mm", "A4", "") pdf.SetMargins(8, 8, 8) @@ -108,7 +115,14 @@ func ExportStatementAgingPDFHandler(_ *sql.DB) http.HandlerFunc { return } - drawStatementAgingPDF(pdf, params, masters) + drawCustomerBalancePDF( + pdf, + selectedDate, + listParams.CariSearch, + detailed, + summaries, + detailsByMaster, + ) if err := pdf.Error(); err != nil { http.Error(w, "pdf render error: "+err.Error(), http.StatusInternalServerError) @@ -122,7 +136,11 @@ func ExportStatementAgingPDFHandler(_ *sql.DB) http.HandlerFunc { } w.Header().Set("Content-Type", "application/pdf") - w.Header().Set("Content-Disposition", `inline; filename="account-aging-detailed.pdf"`) + filename := "account-aging-summary.pdf" + if detailed { + filename = "account-aging-detailed.pdf" + } + w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", filename)) _, _ = w.Write(buf.Bytes()) } } diff --git a/ui/.quasar/prod-spa/app.js b/ui/.quasar/prod-spa/app.js deleted file mode 100644 index caeaac1..0000000 --- a/ui/.quasar/prod-spa/app.js +++ /dev/null @@ -1,75 +0,0 @@ -/* eslint-disable */ -/** - * THIS FILE IS GENERATED AUTOMATICALLY. - * DO NOT EDIT. - * - * You are probably looking on adding startup/initialization code. - * Use "quasar new boot " and add it there. - * One boot file per concern. Then reference the file(s) in quasar.config file > boot: - * boot: ['file', ...] // do not add ".js" extension to it. - * - * Boot files are your "main.js" - **/ - - - - - -import { Quasar } from 'quasar' -import { markRaw } from 'vue' -import RootComponent from 'app/src/App.vue' - -import createStore from 'app/src/stores/index' -import createRouter from 'app/src/router/index' - - - - - -export default async function (createAppFn, quasarUserOptions) { - - - // Create the app instance. - // Here we inject into it the Quasar UI, the router & possibly the store. - const app = createAppFn(RootComponent) - - - - app.use(Quasar, quasarUserOptions) - - - - - const store = typeof createStore === 'function' - ? await createStore({}) - : createStore - - - app.use(store) - - - - - - const router = markRaw( - typeof createRouter === 'function' - ? await createRouter({store}) - : createRouter - ) - - - // make router instance available in store - - store.use(({ store }) => { store.router = router }) - - - - // Expose the app, the router and the store. - // Note that we are not mounting the app here, since bootstrapping will be - // different depending on whether we are in a browser or on the server. - return { - app, - store, - router - } -} diff --git a/ui/.quasar/prod-spa/client-entry.js b/ui/.quasar/prod-spa/client-entry.js deleted file mode 100644 index 5de66d0..0000000 --- a/ui/.quasar/prod-spa/client-entry.js +++ /dev/null @@ -1,154 +0,0 @@ -/* eslint-disable */ -/** - * THIS FILE IS GENERATED AUTOMATICALLY. - * DO NOT EDIT. - * - * You are probably looking on adding startup/initialization code. - * Use "quasar new boot " and add it there. - * One boot file per concern. Then reference the file(s) in quasar.config file > boot: - * boot: ['file', ...] // do not add ".js" extension to it. - * - * Boot files are your "main.js" - **/ - - -import { createApp } from 'vue' - - - - - - - -import '@quasar/extras/roboto-font/roboto-font.css' - -import '@quasar/extras/material-icons/material-icons.css' - - - - -// We load Quasar stylesheet file -import 'quasar/dist/quasar.sass' - - - - -import 'src/css/app.css' - - -import createQuasarApp from './app.js' -import quasarUserOptions from './quasar-user-options.js' - - - - - - - - -const publicPath = `/` - - -async function start ({ - app, - router - , store -}, bootFiles) { - - let hasRedirected = false - const getRedirectUrl = url => { - try { return router.resolve(url).href } - catch (err) {} - - return Object(url) === url - ? null - : url - } - const redirect = url => { - hasRedirected = true - - if (typeof url === 'string' && /^https?:\/\//.test(url)) { - window.location.href = url - return - } - - const href = getRedirectUrl(url) - - // continue if we didn't fail to resolve the url - if (href !== null) { - window.location.href = href - window.location.reload() - } - } - - const urlPath = window.location.href.replace(window.location.origin, '') - - for (let i = 0; hasRedirected === false && i < bootFiles.length; i++) { - try { - await bootFiles[i]({ - app, - router, - store, - ssrContext: null, - redirect, - urlPath, - publicPath - }) - } - catch (err) { - if (err && err.url) { - redirect(err.url) - return - } - - console.error('[Quasar] boot error:', err) - return - } - } - - if (hasRedirected === true) return - - - app.use(router) - - - - - - - app.mount('#q-app') - - - -} - -createQuasarApp(createApp, quasarUserOptions) - - .then(app => { - // eventually remove this when Cordova/Capacitor/Electron support becomes old - const [ method, mapFn ] = Promise.allSettled !== void 0 - ? [ - 'allSettled', - bootFiles => bootFiles.map(result => { - if (result.status === 'rejected') { - console.error('[Quasar] boot error:', result.reason) - return - } - return result.value.default - }) - ] - : [ - 'all', - bootFiles => bootFiles.map(entry => entry.default) - ] - - return Promise[ method ]([ - - import(/* webpackMode: "eager" */ 'boot/dayjs') - - ]).then(bootFiles => { - const boot = mapFn(bootFiles).filter(entry => typeof entry === 'function') - start(app, boot) - }) - }) - diff --git a/ui/.quasar/prod-spa/client-prefetch.js b/ui/.quasar/prod-spa/client-prefetch.js deleted file mode 100644 index 9bbe3c5..0000000 --- a/ui/.quasar/prod-spa/client-prefetch.js +++ /dev/null @@ -1,116 +0,0 @@ -/* eslint-disable */ -/** - * THIS FILE IS GENERATED AUTOMATICALLY. - * DO NOT EDIT. - * - * You are probably looking on adding startup/initialization code. - * Use "quasar new boot " and add it there. - * One boot file per concern. Then reference the file(s) in quasar.config file > boot: - * boot: ['file', ...] // do not add ".js" extension to it. - * - * Boot files are your "main.js" - **/ - - - -import App from 'app/src/App.vue' -let appPrefetch = typeof App.preFetch === 'function' - ? App.preFetch - : ( - // Class components return the component options (and the preFetch hook) inside __c property - App.__c !== void 0 && typeof App.__c.preFetch === 'function' - ? App.__c.preFetch - : false - ) - - -function getMatchedComponents (to, router) { - const route = to - ? (to.matched ? to : router.resolve(to).route) - : router.currentRoute.value - - if (!route) { return [] } - - const matched = route.matched.filter(m => m.components !== void 0) - - if (matched.length === 0) { return [] } - - return Array.prototype.concat.apply([], matched.map(m => { - return Object.keys(m.components).map(key => { - const comp = m.components[key] - return { - path: m.path, - c: comp - } - }) - })) -} - -export function addPreFetchHooks ({ router, store, publicPath }) { - // Add router hook for handling preFetch. - // Doing it after initial route is resolved so that we don't double-fetch - // the data that we already have. Using router.beforeResolve() so that all - // async components are resolved. - router.beforeResolve((to, from, next) => { - const - urlPath = window.location.href.replace(window.location.origin, ''), - matched = getMatchedComponents(to, router), - prevMatched = getMatchedComponents(from, router) - - let diffed = false - const preFetchList = matched - .filter((m, i) => { - return diffed || (diffed = ( - !prevMatched[i] || - prevMatched[i].c !== m.c || - m.path.indexOf('/:') > -1 // does it has params? - )) - }) - .filter(m => m.c !== void 0 && ( - typeof m.c.preFetch === 'function' - // Class components return the component options (and the preFetch hook) inside __c property - || (m.c.__c !== void 0 && typeof m.c.__c.preFetch === 'function') - )) - .map(m => m.c.__c !== void 0 ? m.c.__c.preFetch : m.c.preFetch) - - - if (appPrefetch !== false) { - preFetchList.unshift(appPrefetch) - appPrefetch = false - } - - - if (preFetchList.length === 0) { - return next() - } - - let hasRedirected = false - const redirect = url => { - hasRedirected = true - next(url) - } - const proceed = () => { - - if (hasRedirected === false) { next() } - } - - - - preFetchList.reduce( - (promise, preFetch) => promise.then(() => hasRedirected === false && preFetch({ - store, - currentRoute: to, - previousRoute: from, - redirect, - urlPath, - publicPath - })), - Promise.resolve() - ) - .then(proceed) - .catch(e => { - console.error(e) - proceed() - }) - }) -} diff --git a/ui/.quasar/prod-spa/quasar-user-options.js b/ui/.quasar/prod-spa/quasar-user-options.js deleted file mode 100644 index ac1dae3..0000000 --- a/ui/.quasar/prod-spa/quasar-user-options.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -/** - * THIS FILE IS GENERATED AUTOMATICALLY. - * DO NOT EDIT. - * - * You are probably looking on adding startup/initialization code. - * Use "quasar new boot " and add it there. - * One boot file per concern. Then reference the file(s) in quasar.config file > boot: - * boot: ['file', ...] // do not add ".js" extension to it. - * - * Boot files are your "main.js" - **/ - -import lang from 'quasar/lang/tr.js' - - - -import {Loading,Dialog,Notify} from 'quasar' - - - -export default { config: {"notify":{"position":"top","timeout":2500}},lang,plugins: {Loading,Dialog,Notify} } - diff --git a/ui/quasar.config.js.temporary.compiled.1772488385975.mjs b/ui/quasar.config.js.temporary.compiled.1772520846397.mjs similarity index 100% rename from ui/quasar.config.js.temporary.compiled.1772488385975.mjs rename to ui/quasar.config.js.temporary.compiled.1772520846397.mjs diff --git a/ui/src/css/app.css b/ui/src/css/app.css index 7dbf31d..aacb3d6 100644 --- a/ui/src/css/app.css +++ b/ui/src/css/app.css @@ -303,6 +303,14 @@ body { margin-left: 0; width: 100%; } +@media (max-width: 1023px) { + .body--drawer-left-open .q-page-container, + .body--drawer-left-closed .q-page-container { + margin-left: 0 !important; + width: 100% !important; + } +} + /* 🔸 Yatay scroll sadece grid alanında */ .order-scroll-x { flex: 1; diff --git a/ui/src/layouts/MainLayout.vue b/ui/src/layouts/MainLayout.vue index 86572ca..e1f8ee8 100644 --- a/ui/src/layouts/MainLayout.vue +++ b/ui/src/layouts/MainLayout.vue @@ -129,9 +129,9 @@ + + diff --git a/ui/src/pages/CustomerBalanceList.vue b/ui/src/pages/CustomerBalanceList.vue index 78c35d1..e4a0588 100644 --- a/ui/src/pages/CustomerBalanceList.vue +++ b/ui/src/pages/CustomerBalanceList.vue @@ -1,7 +1,7 @@  + @@ -520,6 +530,7 @@ import { download, extractApiErrorDetail } from 'src/services/api' const store = useCustomerBalanceListStore() const expanded = ref({}) const allDetailsOpen = ref(false) +const filtersCollapsed = ref(false) const $q = useQuasar() const { canRead, canExport } = usePermission() @@ -643,6 +654,10 @@ function onToggle13Changed (val) { } } +function toggleFiltersCollapsed () { + filtersCollapsed.value = !filtersCollapsed.value +} + function toggleGroup (key) { expanded.value[key] = !expanded.value[key] @@ -844,6 +859,28 @@ function formatRowPrBr (row, tip) { padding-bottom: 6px; } +.filter-sticky.collapsed { + padding-bottom: 0; +} + +.top-actions.single-line { + flex-wrap: nowrap; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: thin; + padding-bottom: 4px; +} + +.top-actions.single-line > [class*='col-'], +.top-actions.single-line > .col-auto { + flex: 0 0 auto; + min-width: 220px; +} + +.top-actions.single-line > .col-auto { + min-width: auto; +} + .filters-panel { border: 1px solid rgba(0, 0, 0, 0.12); border-radius: 8px; diff --git a/ui/src/pages/OrderProductionUpdate.vue b/ui/src/pages/OrderProductionUpdate.vue index c30c7e5..49ba160 100644 --- a/ui/src/pages/OrderProductionUpdate.vue +++ b/ui/src/pages/OrderProductionUpdate.vue @@ -11,6 +11,14 @@ :loading="store.loading" @click="refreshAll" /> +
@@ -76,6 +84,27 @@ :rows-per-page-options="[0]" hide-bottom > + + + + @@ -161,9 +193,11 @@ emit-value map-options use-input + new-value-mode="add-unique" dense filled label="Yeni 2. Renk" + @new-value="(val, done) => onCreateSecondColorValue(props.row, val, done)" /> @@ -214,8 +248,10 @@ const descFilter = ref('') const productOptions = ref([]) const productSearch = ref('') const rowSavingId = ref('') +const selectedMap = ref({}) const columns = [ + { name: 'select', label: '', field: 'select', align: 'center', sortable: false, style: 'width:44px;', headerStyle: 'width:44px;' }, { name: 'OldItemCode', label: 'Eski Urun Kodu', field: 'OldItemCode', align: 'left', sortable: true, style: 'min-width:120px;white-space:nowrap', headerStyle: 'min-width:120px;white-space:nowrap', headerClasses: 'col-old', classes: 'col-old' }, { name: 'OldColor', label: 'Eski Urun Rengi', field: 'OldColor', align: 'left', sortable: true, style: 'min-width:100px;white-space:nowrap', headerStyle: 'min-width:100px;white-space:nowrap', headerClasses: 'col-old', classes: 'col-old' }, { name: 'OldDim2', label: 'Eski 2. Renk', field: 'OldDim2', align: 'left', sortable: true, style: 'min-width:90px;white-space:nowrap', headerStyle: 'min-width:90px;white-space:nowrap', headerClasses: 'col-old', classes: 'col-old' }, @@ -274,18 +310,21 @@ const filteredRows = computed(() => { ) }) +const visibleRowKeys = computed(() => filteredRows.value.map(r => r.RowKey)) +const selectedVisibleCount = computed(() => visibleRowKeys.value.filter(k => !!selectedMap.value[k]).length) +const allSelectedVisible = computed(() => visibleRowKeys.value.length > 0 && selectedVisibleCount.value === visibleRowKeys.value.length) +const someSelectedVisible = computed(() => selectedVisibleCount.value > 0) + function onSelectProduct (row, code) { productSearch.value = '' onNewItemChange(row, code) } function onNewItemChange (row, val) { - const next = String(val || '').trim() - if (next && !isValidModelCode(next)) { - $q.notify({ type: 'negative', message: 'Model kodu formati gecersiz. Ornek: S000-DMY00001' }) - row.NewItemCode = '' - row.NewColor = '' - row.NewDim2 = '' + const next = String(val || '').trim().toUpperCase() + if (next.length > 13) { + $q.notify({ type: 'negative', message: 'Model kodu en fazla 13 karakter olabilir.' }) + row.NewItemCode = next.slice(0, 13) return } row.NewItemCode = next ? next.toUpperCase() : '' @@ -297,6 +336,7 @@ function onNewItemChange (row, val) { } function onNewColorChange (row) { + row.NewColor = normalizeShortCode(row.NewColor, 3) row.NewDim2 = '' if (row.NewItemCode && row.NewColor) { store.fetchSecondColors(row.NewItemCode, row.NewColor) @@ -319,9 +359,91 @@ function getSecondColorOptions (row) { return store.secondColorOptionsByKey[key] || [] } -function isValidModelCode (value) { - const text = String(value || '').trim().toUpperCase() - return /^[A-Z][0-9]{3}-[A-Z]{3}[0-9]{5}$/.test(text) +function toggleRowSelection (rowKey, checked) { + const next = { ...selectedMap.value } + if (checked) next[rowKey] = true + else delete next[rowKey] + selectedMap.value = next +} + +function toggleSelectAllVisible (checked) { + const next = { ...selectedMap.value } + for (const key of visibleRowKeys.value) { + if (checked) next[key] = true + else delete next[key] + } + selectedMap.value = next +} + +function onCreateColorValue (row, val, done) { + const code = normalizeShortCode(val, 3) + if (!code) { + done(null) + return + } + row.NewColor = code + onNewColorChange(row) + done(code, 'add-unique') +} + +function onCreateSecondColorValue (row, val, done) { + const code = normalizeShortCode(val, 3) + if (!code) { + done(null) + return + } + row.NewDim2 = code + done(code, 'add-unique') +} + +function normalizeShortCode (value, maxLen) { + return String(value || '').trim().toUpperCase().slice(0, maxLen) +} + +function validateRowInput (row) { + const newItemCode = String(row.NewItemCode || '').trim().toUpperCase() + const newColor = normalizeShortCode(row.NewColor, 3) + const newDim2 = normalizeShortCode(row.NewDim2, 3) + const oldColor = String(row.OldColor || '').trim() + const oldDim2 = String(row.OldDim2 || '').trim() + + if (!newItemCode) return 'Yeni model kodu zorunludur.' + if (newItemCode.length !== 13) return 'Yeni model kodu 13 karakter olmalidir.' + if (oldColor && !newColor) return 'Eski kayitta 1. renk oldugu icin yeni 1. renk zorunludur.' + if (newColor && newColor.length !== 3) return 'Yeni 1. renk kodu 3 karakter olmalidir.' + if (oldDim2 && !newDim2) return 'Eski kayitta 2. renk oldugu icin yeni 2. renk zorunludur.' + if (newDim2 && newDim2.length !== 3) return 'Yeni 2. renk kodu 3 karakter olmalidir.' + if (newDim2 && !newColor) return '2. renk girmek icin 1. renk zorunludur.' + + row.NewItemCode = newItemCode + row.NewColor = newColor + row.NewDim2 = newDim2 + return '' +} + +function collectLinesFromRows (selectedRows) { + const lines = [] + for (const row of selectedRows) { + const errMsg = validateRowInput(row) + if (errMsg) { + return { errMsg, lines: [] } + } + + const baseLine = { + NewItemCode: String(row.NewItemCode || '').trim().toUpperCase(), + NewColor: normalizeShortCode(row.NewColor, 3), + NewDim2: normalizeShortCode(row.NewDim2, 3), + NewDesc: String((row.NewDesc || row.OldDesc) || '').trim() + } + + for (const id of (row.OrderLineIDs || [])) { + lines.push({ + OrderLineID: id, + ...baseLine + }) + } + } + return { errMsg: '', lines } } function buildGroupKey (item) { @@ -416,23 +538,12 @@ async function refreshAll () { } async function onRowSubmit (row) { - const baseLine = { - NewItemCode: String(row.NewItemCode || '').trim(), - NewColor: String(row.NewColor || '').trim(), - NewDim2: String(row.NewDim2 || '').trim(), - NewDesc: String((row.NewDesc || row.OldDesc) || '').trim() - } - - if (!baseLine.NewItemCode || !baseLine.NewColor) { - $q.notify({ type: 'negative', message: 'Yeni urun ve renk zorunludur.' }) + const { errMsg, lines } = collectLinesFromRows([row]) + if (errMsg) { + $q.notify({ type: 'negative', message: errMsg }) return } - const lines = (row.OrderLineIDs || []).map(id => ({ - OrderLineID: id, - ...baseLine - })) - if (!lines.length) { $q.notify({ type: 'negative', message: 'Satir bulunamadi.' }) return @@ -467,6 +578,52 @@ async function onRowSubmit (row) { rowSavingId.value = '' } } + +async function onBulkSubmit () { + const selectedRows = rows.value.filter(r => !!selectedMap.value[r.RowKey]) + if (!selectedRows.length) { + $q.notify({ type: 'warning', message: 'Lutfen en az bir satir seciniz.' }) + return + } + + const { errMsg, lines } = collectLinesFromRows(selectedRows) + if (errMsg) { + $q.notify({ type: 'negative', message: errMsg }) + return + } + if (!lines.length) { + $q.notify({ type: 'negative', message: 'Secili satirlarda guncellenecek kayit bulunamadi.' }) + return + } + + try { + const validate = await store.validateUpdates(orderHeaderID.value, lines) + const missingCount = validate?.missingCount || 0 + if (missingCount > 0) { + const missingList = (validate?.missing || []).map(v => ( + `${v.ItemCode} / ${v.ColorCode} / ${v.ItemDim1Code} / ${v.ItemDim2Code}` + )) + $q.dialog({ + title: 'Eksik Varyantlar', + message: `Eksik varyant bulundu: ${missingCount}

${missingList.join('
')}`, + html: true, + ok: { label: 'Ekle ve Guncelle', color: 'primary' }, + cancel: { label: 'Vazgec', flat: true } + }).onOk(async () => { + await store.applyUpdates(orderHeaderID.value, lines, true) + await store.fetchItems(orderHeaderID.value) + selectedMap.value = {} + }) + return + } + + await store.applyUpdates(orderHeaderID.value, lines, false) + await store.fetchItems(orderHeaderID.value) + selectedMap.value = {} + } catch (err) { + $q.notify({ type: 'negative', message: 'Toplu kayit islemi basarisiz.' }) + } +}