From 4d8a659650576b5ff3b0815b48e246cd8a0e7da4 Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Thu, 18 Jun 2026 18:33:38 +0300 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- svc/main.go | 30 + svc/routes/mail_pdf_table.go | 51 ++ svc/routes/product_pricing_change_mail.go | 117 ++-- svc/routes/wholesale_campaign_mail.go | 86 ++- ui/src/layouts/MainLayout.vue | 5 + ui/src/pages/OrderPriceList.vue | 757 ++++++++++++++++++++++ ui/src/router/routes.js | 6 + 7 files changed, 984 insertions(+), 68 deletions(-) create mode 100644 svc/routes/mail_pdf_table.go create mode 100644 ui/src/pages/OrderPriceList.vue diff --git a/svc/main.go b/svc/main.go index ace6536..ce3c7fa 100644 --- a/svc/main.go +++ b/svc/main.go @@ -785,6 +785,36 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "order", "view", http.HandlerFunc(routes.GetProductImageContentHandler(pgDB)), ) + bindV3(r, pgDB, + "/api/order/price-list/products", "GET", + "order", "view", + wrapV3(http.HandlerFunc(routes.GetProductPricingListHandler)), + ) + bindV3(r, pgDB, + "/api/order/price-list/options", "GET", + "order", "view", + wrapV3(http.HandlerFunc(routes.GetProductPricingFilterOptionsHandler)), + ) + bindV3(r, pgDB, + "/api/order/price-list/campaigns", "GET", + "order", "view", + wrapV3(routes.GetWholesaleCampaignsHandler(pgDB)), + ) + bindV3(r, pgDB, + "/api/order/price-list/variant-rows", "GET", + "order", "view", + wrapV3(routes.GetWholesaleCampaignVariantRowsHandler(pgDB, mssql)), + ) + bindV3(r, pgDB, + "/api/order/price-list/export-excel", "POST", + "order", "view", + wrapV3(http.HandlerFunc(routes.ExportProductPriceListExcelHandler(pgDB))), + ) + bindV3(r, pgDB, + "/api/order/price-list/export-pdf", "POST", + "order", "view", + wrapV3(http.HandlerFunc(routes.ExportProductPriceListPDFHandler(pgDB))), + ) bindV3(r, pgDB, "/api/product-size-match/rules", "GET", "order", "view", diff --git a/svc/routes/mail_pdf_table.go b/svc/routes/mail_pdf_table.go new file mode 100644 index 0000000..a3f4e32 --- /dev/null +++ b/svc/routes/mail_pdf_table.go @@ -0,0 +1,51 @@ +package routes + +import ( + "strings" + + "github.com/jung-kurt/gofpdf" +) + +func writePDFTableHeader(pdf *gofpdf.Fpdf, font string, heads []string, widths []float64) { + pdf.SetFont(font, "B", 5.9) + pdf.SetFillColor(235, 235, 235) + for i, h := range heads { + w := 12.0 + if i < len(widths) { + w = widths[i] + } + pdf.CellFormat(w, 4.8, fitPDFCellText(pdf, h, w-1.2), "1", 0, "L", true, 0, "") + } + pdf.Ln(-1) +} + +func writePDFTableRow(pdf *gofpdf.Fpdf, cells []string, widths []float64, aligns []string, height float64) { + for i, c := range cells { + w := 12.0 + if i < len(widths) { + w = widths[i] + } + align := "L" + if i < len(aligns) && strings.TrimSpace(aligns[i]) != "" { + align = aligns[i] + } + pdf.CellFormat(w, height, fitPDFCellText(pdf, strings.TrimSpace(c), w-1.2), "1", 0, align, false, 0, "") + } + pdf.Ln(-1) +} + +func fitPDFCellText(pdf *gofpdf.Fpdf, s string, maxWidth float64) string { + s = strings.TrimSpace(s) + if s == "" || pdf.GetStringWidth(s) <= maxWidth { + return s + } + r := []rune(s) + for len(r) > 0 { + candidate := string(r) + "..." + if pdf.GetStringWidth(candidate) <= maxWidth { + return candidate + } + r = r[:len(r)-1] + } + return "" +} diff --git a/svc/routes/product_pricing_change_mail.go b/svc/routes/product_pricing_change_mail.go index c3f4da0..fa1cefc 100644 --- a/svc/routes/product_pricing_change_mail.go +++ b/svc/routes/product_pricing_change_mail.go @@ -1,6 +1,7 @@ package routes import ( + "bytes" "context" "database/sql" "fmt" @@ -13,6 +14,8 @@ import ( "bssapp-backend/internal/mailer" "bssapp-backend/models" "bssapp-backend/queries" + + "github.com/jung-kurt/gofpdf" ) func loadPricingRecipients(pg *sql.DB, firstGroupCode string) ([]string, error) { @@ -79,42 +82,77 @@ func fmtDateTRFromISO(d string) string { return day + "." + m + "." + y } -func buildPricingChangeMailHTML(firstGroupCode string, rows []models.ProductPricing, actor string, at time.Time) string { - // Keep it simple: wide, scrollable table. +func buildPricingChangeMailHTML(rows []models.ProductPricing, actor string, at time.Time) string { var b strings.Builder - // NOTE: Mail clients often render small fonts; keep this comfortably readable. - // Use large inline sizes (some clients still downscale); keep everything inline for maximum compatibility. b.WriteString(`
`) b.WriteString(`
`) b.WriteString(`
Fiyat Degisikligi
`) - b.WriteString(`
Urun Ilk Grubu: ` + htmlEscapeMini(firstGroupCode) + `
`) if strings.TrimSpace(actor) != "" { b.WriteString(`
Islem Yapan: ` + htmlEscapeMini(actor) + `
`) } b.WriteString(`
Tarih: ` + htmlEscapeMini(at.Format("02.01.2006 15:04")) + `
`) b.WriteString(`
Urun Sayisi: ` + fmt.Sprintf("%d", len(rows)) + `
`) + b.WriteString(`
Detaylar ekteki PDF dosyasindadir.
`) b.WriteString(`
`) + b.WriteString(`
`) + return b.String() +} - b.WriteString(`
`) - b.WriteString(``) - b.WriteString(``) +func buildPricingChangeMailPDF(rows []models.ProductPricing, actor string, at time.Time) ([]byte, error) { + pdf := gofpdf.New("L", "mm", "A2", "") + font := "Arial" + if err := registerDejavuFonts(pdf, "dejavu"); err == nil { + font = "dejavu" + } else { + log.Printf("[pricing-mail] pdf font fallback: %v", err) + } + pdf.SetMargins(7, 8, 7) + pdf.SetAutoPageBreak(true, 10) + pdf.AddPage() + pdf.SetFont(font, "B", 13) + pdf.CellFormat(0, 7, "Fiyat Degisikligi", "", 1, "L", false, 0, "") + pdf.SetFont(font, "", 8) + if strings.TrimSpace(actor) != "" { + pdf.CellFormat(0, 5, "Islem Yapan: "+strings.TrimSpace(actor), "", 1, "L", false, 0, "") + } + pdf.CellFormat(0, 5, "Tarih: "+at.Format("02.01.2006 15:04"), "", 1, "L", false, 0, "") + pdf.CellFormat(0, 5, fmt.Sprintf("Urun Sayisi: %d", len(rows)), "", 1, "L", false, 0, "") + pdf.Ln(2) heads := []string{ - "MARKA GRUBU", "MARKA", "BRAND CODE", "URUN KODU", - "STOK ADET", "STOK GIRIS", "SON MALIYET", "SON FIYAT", - "ASKILI YAN", "KATEGORI", "URUN ILK GRUBU", "URUN ANA GRUBU", "URUN ALT GRUBU", "ICERIK", "KARISIM", - "MALIYET FIYATI", "TABAN USD", "TABAN TRY", + "MARKA GRUBU", "MARKA", "BRAND", "URUN KODU", + "STOK", "STOK GIRIS", "SON MALIYET", "SON FIYAT", + "ASKILI", "KATEGORI", "URUN ANA GRUBU", "URUN ALT GRUBU", "ICERIK", "KARISIM", + "MALIYET", "TABAN USD", "TABAN TRY", "USD1", "USD2", "USD3", "USD4", "USD5", "USD6", "EUR1", "EUR2", "EUR3", "EUR4", "EUR5", "EUR6", "TRY1", "TRY2", "TRY3", "TRY4", "TRY5", "TRY6", } - for _, h := range heads { - b.WriteString(``) + widths := []float64{ + 20, 18, 18, 28, + 12, 18, 18, 18, + 14, 18, 22, 22, 20, 20, + 18, 18, 18, + 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, } - b.WriteString(``) + aligns := make([]string, len(heads)) + for i := range aligns { + aligns[i] = "L" + } + for _, idx := range []int{4, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34} { + aligns[idx] = "R" + } + writePDFTableHeader(pdf, font, heads, widths) + pdf.SetFont(font, "", 5.7) for _, r := range rows { - b.WriteString(``) + if pdf.GetY() > 405 { + pdf.AddPage() + writePDFTableHeader(pdf, font, heads, widths) + pdf.SetFont(font, "", 5.7) + } cells := []string{ r.BrandGroupSec, r.Marka, @@ -126,7 +164,6 @@ func buildPricingChangeMailHTML(firstGroupCode string, rows []models.ProductPric fmtDateTRFromISO(r.LastPricingDate), r.AskiliYan, r.Kategori, - r.UrunIlkGrubu, r.UrunAnaGrubu, r.UrunAltGrubu, r.Icerik, @@ -138,27 +175,14 @@ func buildPricingChangeMailHTML(firstGroupCode string, rows []models.ProductPric fmtMoneyMail(r.EUR1), fmtMoneyMail(r.EUR2), fmtMoneyMail(r.EUR3), fmtMoneyMail(r.EUR4), fmtMoneyMail(r.EUR5), fmtMoneyMail(r.EUR6), fmtMoneyMail(r.TRY1), fmtMoneyMail(r.TRY2), fmtMoneyMail(r.TRY3), fmtMoneyMail(r.TRY4), fmtMoneyMail(r.TRY5), fmtMoneyMail(r.TRY6), } - for i, c := range cells { - align := "left" - // right align numeric-ish cells - if i >= 4 { - switch i { - case 4, 15, 16, 17, - 18, 19, 20, 21, 22, 23, - 24, 25, 26, 27, 28, 29, - 30, 31, 32, 33, 34, 35: - align = "right" - } - } - b.WriteString(``) - } - b.WriteString(``) + writePDFTableRow(pdf, cells, widths, aligns, 4.4) } - b.WriteString(`
` + htmlEscapeMini(h) + `
` + htmlEscapeMini(strings.TrimSpace(c)) + `
`) - b.WriteString(`
Bu e-posta BSSApp sistemi tarafindan otomatik olusturulmustur.
`) - b.WriteString(``) - return b.String() + var buf bytes.Buffer + if err := pdf.Output(&buf); err != nil { + return nil, err + } + return buf.Bytes(), nil } // sendPricingChangeMails sends one mail per UrunIlkGrubu (group) based on mk_pricing_first_group_mail mapping. @@ -233,8 +257,18 @@ func sendPricingChangeMails(bg context.Context, ml *mailer.GraphMailer, productC return strings.TrimSpace(list[i].ProductCode) < strings.TrimSpace(list[j].ProductCode) }) - subject := fmt.Sprintf("Fiyat Degisikligi | %s | %s | %d urun", group, now.Format("02.01.2006 15:04"), len(list)) - html := buildPricingChangeMailHTML(group, list, actor, now) + subject := fmt.Sprintf("Fiyat Degisikligi | %s | %d urun", now.Format("02.01.2006 15:04"), len(list)) + html := buildPricingChangeMailHTML(list, actor, now) + attachments := []mailer.Attachment(nil) + if data, err := buildPricingChangeMailPDF(list, actor, now); err != nil { + log.Printf("[pricing-mail] pdf build failed group=%s err=%v", group, err) + } else { + attachments = append(attachments, mailer.Attachment{ + FileName: fmt.Sprintf("fiyat-degisikligi-%s.pdf", now.Format("20060102-1504")), + ContentType: "application/pdf", + Data: data, + }) + } // Retry 2 times with backoff. backoff := []time.Duration{800 * time.Millisecond, 2500 * time.Millisecond} @@ -245,9 +279,10 @@ func sendPricingChangeMails(bg context.Context, ml *mailer.GraphMailer, productC } stepCtx, stepCancel := context.WithTimeout(bg, 25*time.Second) err := ml.Send(stepCtx, mailer.Message{ - To: recipients, - Subject: subject, - BodyHTML: html, + To: recipients, + Subject: subject, + BodyHTML: html, + Attachments: attachments, }) stepCancel() if err == nil { diff --git a/svc/routes/wholesale_campaign_mail.go b/svc/routes/wholesale_campaign_mail.go index 8eeaa11..18873c8 100644 --- a/svc/routes/wholesale_campaign_mail.go +++ b/svc/routes/wholesale_campaign_mail.go @@ -1,6 +1,7 @@ package routes import ( + "bytes" "context" "database/sql" "fmt" @@ -14,6 +15,7 @@ import ( "bssapp-backend/models" "bssapp-backend/queries" + "github.com/jung-kurt/gofpdf" "github.com/lib/pq" ) @@ -31,22 +33,43 @@ type wholesaleCampaignMailRow struct { DiscountRate float64 } -func buildWholesaleCampaignChangeMailHTML(firstGroupCode string, rows []wholesaleCampaignMailRow, actor string, at time.Time) string { +func buildWholesaleCampaignChangeMailHTML(rows []wholesaleCampaignMailRow, actor string, at time.Time) string { var b strings.Builder b.WriteString(`
`) b.WriteString(`
`) b.WriteString(`
Kampanya Degisikligi
`) - b.WriteString(`
Urun Ilk Grubu: ` + htmlEscapeMini(firstGroupCode) + `
`) if strings.TrimSpace(actor) != "" { b.WriteString(`
Islem Yapan: ` + htmlEscapeMini(actor) + `
`) } b.WriteString(`
Tarih: ` + htmlEscapeMini(at.Format("02.01.2006 15:04")) + `
`) b.WriteString(`
Varyant Sayisi: ` + fmt.Sprintf("%d", len(rows)) + `
`) + b.WriteString(`
Detaylar ekteki PDF dosyasindadir.
`) b.WriteString(`
`) + b.WriteString(`
`) + return b.String() +} + +func buildWholesaleCampaignChangeMailPDF(rows []wholesaleCampaignMailRow, actor string, at time.Time) ([]byte, error) { + pdf := gofpdf.New("L", "mm", "A4", "") + font := "Arial" + if err := registerDejavuFonts(pdf, "dejavu"); err == nil { + font = "dejavu" + } else { + log.Printf("[campaign-mail] pdf font fallback: %v", err) + } + pdf.SetMargins(7, 8, 7) + pdf.SetAutoPageBreak(true, 10) + pdf.AddPage() + pdf.SetFont(font, "B", 13) + pdf.CellFormat(0, 7, "Kampanya Degisikligi", "", 1, "L", false, 0, "") + pdf.SetFont(font, "", 8) + if strings.TrimSpace(actor) != "" { + pdf.CellFormat(0, 5, "Islem Yapan: "+strings.TrimSpace(actor), "", 1, "L", false, 0, "") + } + pdf.CellFormat(0, 5, "Tarih: "+at.Format("02.01.2006 15:04"), "", 1, "L", false, 0, "") + pdf.CellFormat(0, 5, fmt.Sprintf("Varyant Sayisi: %d", len(rows)), "", 1, "L", false, 0, "") + pdf.Ln(2) - b.WriteString(`
`) - b.WriteString(``) - b.WriteString(``) heads := []string{ "MARKA GRUBU", "MARKA", @@ -56,13 +79,17 @@ func buildWholesaleCampaignChangeMailHTML(firstGroupCode string, rows []wholesal "KAMPANYA", "IND %", } - for _, h := range heads { - b.WriteString(``) - } - b.WriteString(``) + widths := []float64{35, 26, 35, 20, 20, 118, 16} + aligns := []string{"L", "L", "L", "R", "R", "L", "R"} + writePDFTableHeader(pdf, font, heads, widths) + pdf.SetFont(font, "", 7) for _, r := range rows { - b.WriteString(``) + if pdf.GetY() > 190 { + pdf.AddPage() + writePDFTableHeader(pdf, font, heads, widths) + pdf.SetFont(font, "", 7) + } campaignLabel := strings.TrimSpace(r.CampaignCode) if t := strings.TrimSpace(r.CampaignTitle); t != "" { if campaignLabel != "" { @@ -85,20 +112,14 @@ func buildWholesaleCampaignChangeMailHTML(firstGroupCode string, rows []wholesal campaignLabel, fmt.Sprintf("%.2f", r.DiscountRate), } - for i, c := range cells { - align := "left" - if i == 3 || i == 4 || i == 6 { - align = "right" - } - b.WriteString(``) - } - b.WriteString(``) + writePDFTableRow(pdf, cells, widths, aligns, 5) } - b.WriteString(`
` + htmlEscapeMini(h) + `
` + htmlEscapeMini(strings.TrimSpace(c)) + `
`) - b.WriteString(`
Bu e-posta BSSApp sistemi tarafindan otomatik olusturulmustur.
`) - b.WriteString(``) - return b.String() + var buf bytes.Buffer + if err := pdf.Output(&buf); err != nil { + return nil, err + } + return buf.Bytes(), nil } // sendWholesaleCampaignChangeMails sends one mail per UrunIlkGrubu using existing pricing mail mapping tables. @@ -334,14 +355,25 @@ ORDER BY dim_id, updated_at DESC; return list[i].Dim3 < list[j].Dim3 }) - subject := fmt.Sprintf("Kampanya Degisikligi | %s | %s | %d varyant", group, now.Format("02.01.2006 15:04"), len(list)) - html := buildWholesaleCampaignChangeMailHTML(group, list, actor, now) + subject := fmt.Sprintf("Kampanya Degisikligi | %s | %d varyant", now.Format("02.01.2006 15:04"), len(list)) + html := buildWholesaleCampaignChangeMailHTML(list, actor, now) + attachments := []mailer.Attachment(nil) + if data, err := buildWholesaleCampaignChangeMailPDF(list, actor, now); err != nil { + log.Printf("[campaign-mail] pdf build failed group=%s err=%v", group, err) + } else { + attachments = append(attachments, mailer.Attachment{ + FileName: fmt.Sprintf("kampanya-degisikligi-%s.pdf", now.Format("20060102-1504")), + ContentType: "application/pdf", + Data: data, + }) + } stepCtx, stepCancel := context.WithTimeout(bg, 25*time.Second) err = ml.Send(stepCtx, mailer.Message{ - To: recipients, - Subject: subject, - BodyHTML: html, + To: recipients, + Subject: subject, + BodyHTML: html, + Attachments: attachments, }) stepCancel() if err != nil { diff --git a/ui/src/layouts/MainLayout.vue b/ui/src/layouts/MainLayout.vue index 5b8e35e..85cc30f 100644 --- a/ui/src/layouts/MainLayout.vue +++ b/ui/src/layouts/MainLayout.vue @@ -294,6 +294,11 @@ const menuItems = [ to: '/app/order-gateway', permission: 'order:view' }, + { + label: 'Fiyat Listesi', + to: '/app/order-price-list', + permission: 'order:view' + }, { label: 'Üretime Verilen Siparişleri Güncelle', to: '/app/orderproductionupdate', diff --git a/ui/src/pages/OrderPriceList.vue b/ui/src/pages/OrderPriceList.vue new file mode 100644 index 0000000..08cc20b --- /dev/null +++ b/ui/src/pages/OrderPriceList.vue @@ -0,0 +1,757 @@ + + + + + diff --git a/ui/src/router/routes.js b/ui/src/router/routes.js index 457cfe0..eb40dda 100644 --- a/ui/src/router/routes.js +++ b/ui/src/router/routes.js @@ -317,6 +317,12 @@ const routes = [ component: () => import('pages/OrderBulkClose.vue'), meta: { permission: 'order:update' } }, + { + path: 'order-price-list', + name: 'order-price-list', + component: () => import('pages/OrderPriceList.vue'), + meta: { permission: 'order:view' } + }, { path: 'order-pdf/:id',