Merge remote-tracking branch 'origin/master'
This commit is contained in:
30
svc/main.go
30
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",
|
||||
|
||||
51
svc/routes/mail_pdf_table.go
Normal file
51
svc/routes/mail_pdf_table.go
Normal 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 ""
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user