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(`| ` + htmlEscapeMini(h) + ` | `)
+ 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(`| ` + htmlEscapeMini(strings.TrimSpace(c)) + ` | `)
- }
- b.WriteString(`
`)
+ writePDFTableRow(pdf, cells, widths, aligns, 4.4)
}
- b.WriteString(`
`)
- 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(`| ` + htmlEscapeMini(h) + ` | `)
- }
- 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(`| ` + htmlEscapeMini(strings.TrimSpace(c)) + ` | `)
- }
- b.WriteString(`
`)
+ writePDFTableRow(pdf, cells, widths, aligns, 5)
}
- b.WriteString(`
`)
- 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 @@
+
+
+
+
+
+
+
+
Fiyat Listesi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Excel'e Aktar
+
+
+
+ PDF / Yazdir
+
+
+
+
+
+
+
+
+ Sayfa {{ currentPage }} / {{ Math.max(1, totalPages || 1) }} - {{ filteredRows.length }} satir
+
+
+
+
+
+
+
+
+
Liste Icin Filtre Secin
+
+ Urun Ilk Grubu, Urun Ana Grubu veya Urun Kodu secip LISTEYI GETIR'e basin.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+ {{ formatPrice(props.row[name]) }}
+
+
+
+
+
+
+
+
+
+
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',