Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-06-18 18:33:38 +03:00
parent 6fc7313ae6
commit 4d8a659650
7 changed files with 984 additions and 68 deletions

View File

@@ -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 ""
}

View File

@@ -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(`<div style="font-family:Segoe UI, Arial, sans-serif; font-size:18px; line-height:1.35; -webkit-text-size-adjust:100%;">`)
b.WriteString(`<div style="margin-bottom:10px;">`)
b.WriteString(`<div style="font-size:22px; margin-bottom:4px;"><b>Fiyat Degisikligi</b></div>`)
b.WriteString(`<div>Urun Ilk Grubu: <b>` + htmlEscapeMini(firstGroupCode) + `</b></div>`)
if strings.TrimSpace(actor) != "" {
b.WriteString(`<div>Islem Yapan: <b>` + htmlEscapeMini(actor) + `</b></div>`)
}
b.WriteString(`<div>Tarih: <b>` + htmlEscapeMini(at.Format("02.01.2006 15:04")) + `</b></div>`)
b.WriteString(`<div>Urun Sayisi: <b>` + fmt.Sprintf("%d", len(rows)) + `</b></div>`)
b.WriteString(`<div style="margin-top:10px;">Detaylar ekteki PDF dosyasindadir.</div>`)
b.WriteString(`</div>`)
b.WriteString(`</div>`)
return b.String()
}
b.WriteString(`<div style="max-width:100%; overflow-x:auto;">`)
b.WriteString(`<table style="border-collapse:collapse; font-size:16px; white-space:nowrap;">`)
b.WriteString(`<thead><tr>`)
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(`<th style="border:1px solid #d0d0d0; background:#f3f3f3; padding:8px 10px; text-align:left; font-size:16px;">` + htmlEscapeMini(h) + `</th>`)
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(`</tr></thead><tbody>`)
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(`<tr>`)
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(`<td style="border:1px solid #e0e0e0; padding:8px 10px; text-align:` + align + `;">` + htmlEscapeMini(strings.TrimSpace(c)) + `</td>`)
}
b.WriteString(`</tr>`)
writePDFTableRow(pdf, cells, widths, aligns, 4.4)
}
b.WriteString(`</tbody></table></div>`)
b.WriteString(`<div style="margin-top:12px; font-size:14px; color:#666;">Bu e-posta BSSApp sistemi tarafindan otomatik olusturulmustur.</div>`)
b.WriteString(`</div>`)
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 {

View File

@@ -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(`<div style="font-family:Segoe UI, Arial, sans-serif; font-size:18px; line-height:1.35; -webkit-text-size-adjust:100%;">`)
b.WriteString(`<div style="margin-bottom:10px;">`)
b.WriteString(`<div style="font-size:22px; margin-bottom:4px;"><b>Kampanya Degisikligi</b></div>`)
b.WriteString(`<div>Urun Ilk Grubu: <b>` + htmlEscapeMini(firstGroupCode) + `</b></div>`)
if strings.TrimSpace(actor) != "" {
b.WriteString(`<div>Islem Yapan: <b>` + htmlEscapeMini(actor) + `</b></div>`)
}
b.WriteString(`<div>Tarih: <b>` + htmlEscapeMini(at.Format("02.01.2006 15:04")) + `</b></div>`)
b.WriteString(`<div>Varyant Sayisi: <b>` + fmt.Sprintf("%d", len(rows)) + `</b></div>`)
b.WriteString(`<div style="margin-top:10px;">Detaylar ekteki PDF dosyasindadir.</div>`)
b.WriteString(`</div>`)
b.WriteString(`</div>`)
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(`<div style="max-width:100%; overflow-x:auto;">`)
b.WriteString(`<table style="border-collapse:collapse; font-size:16px; white-space:nowrap;">`)
b.WriteString(`<thead><tr>`)
heads := []string{
"MARKA GRUBU",
"MARKA",
@@ -56,13 +79,17 @@ func buildWholesaleCampaignChangeMailHTML(firstGroupCode string, rows []wholesal
"KAMPANYA",
"IND %",
}
for _, h := range heads {
b.WriteString(`<th style="border:1px solid #d0d0d0; background:#f3f3f3; padding:8px 10px; text-align:left; font-size:16px;">` + htmlEscapeMini(h) + `</th>`)
}
b.WriteString(`</tr></thead><tbody>`)
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(`<tr>`)
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(`<td style="border:1px solid #e0e0e0; padding:8px 10px; text-align:` + align + `;">` + htmlEscapeMini(strings.TrimSpace(c)) + `</td>`)
}
b.WriteString(`</tr>`)
writePDFTableRow(pdf, cells, widths, aligns, 5)
}
b.WriteString(`</tbody></table></div>`)
b.WriteString(`<div style="margin-top:12px; font-size:14px; color:#666;">Bu e-posta BSSApp sistemi tarafindan otomatik olusturulmustur.</div>`)
b.WriteString(`</div>`)
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 {