Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-03-23 09:58:44 +03:00
parent 5eab36df69
commit c0053d6058
7 changed files with 345 additions and 14 deletions

View File

@@ -3,6 +3,7 @@ package mailer
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
@@ -145,6 +146,36 @@ func (g *GraphMailer) Send(ctx context.Context, msg Message) error {
message["replyTo"] = replyToRecipients
}
if len(msg.Attachments) > 0 {
atts := make([]map[string]any, 0, len(msg.Attachments))
for _, a := range msg.Attachments {
if len(a.Data) == 0 {
continue
}
name := strings.TrimSpace(a.FileName)
if name == "" {
name = "attachment.bin"
}
contentType := strings.TrimSpace(a.ContentType)
if contentType == "" {
contentType = "application/octet-stream"
}
atts = append(atts, map[string]any{
"@odata.type": "#microsoft.graph.fileAttachment",
"name": name,
"contentType": contentType,
"contentBytes": base64.StdEncoding.EncodeToString(a.Data),
})
}
if len(atts) > 0 {
message["attachments"] = atts
}
}
payload := map[string]any{
"message": message,
"saveToSentItems": true,

View File

@@ -22,6 +22,13 @@ type Message struct {
Subject string
Body string
BodyHTML string
Attachments []Attachment
}
type Attachment struct {
FileName string
ContentType string
Data []byte
}
func New(cfg Config) *Mailer {

View File

@@ -527,6 +527,7 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
{"/api/order/check/{id}", "GET", "view", routes.OrderExistsHandler(mssql)},
{"/api/order/validate", "POST", "insert", routes.ValidateOrderHandler(mssql)},
{"/api/order/pdf/{id}", "GET", "export", routes.OrderPDFHandler(mssql)},
{"/api/order/send-market-mail", "POST", "update", routes.SendOrderMarketMailHandler(pgDB, mssql, ml)},
{"/api/order-inventory", "GET", "view", http.HandlerFunc(routes.GetOrderInventoryHandler)},
{"/api/orderpricelistb2b", "GET", "view", routes.GetOrderPriceListB2BHandler(pgDB, mssql)},
{"/api/min-price", "GET", "view", routes.GetOrderPriceListB2BHandler(pgDB, mssql)},

258
svc/routes/order_mail.go Normal file
View File

@@ -0,0 +1,258 @@
package routes
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"fmt"
"net/http"
"strings"
"bssapp-backend/internal/mailer"
)
type sendOrderMarketMailPayload struct {
OrderHeaderID string `json:"orderHeaderID"`
}
func SendOrderMarketMailHandler(pg *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if ml == nil {
http.Error(w, "mailer not initialized", http.StatusServiceUnavailable)
return
}
if pg == nil || mssql == nil {
http.Error(w, "database not initialized", http.StatusInternalServerError)
return
}
var payload sendOrderMarketMailPayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "invalid payload", http.StatusBadRequest)
return
}
orderID := strings.TrimSpace(payload.OrderHeaderID)
if orderID == "" {
http.Error(w, "orderHeaderID is required", http.StatusBadRequest)
return
}
orderNo, currAccCode, marketCode, marketTitle, err := resolveOrderMailContext(mssql, orderID)
if err != nil {
http.Error(w, "order context error: "+err.Error(), http.StatusInternalServerError)
return
}
if strings.TrimSpace(marketCode) == "" && strings.TrimSpace(marketTitle) == "" {
http.Error(w, "market not found for order/cari", http.StatusBadRequest)
return
}
recipients, err := loadMarketRecipients(pg, marketCode, marketTitle)
if err != nil {
http.Error(w, "recipient query error: "+err.Error(), http.StatusInternalServerError)
return
}
if len(recipients) == 0 {
http.Error(w, "no active email mapping for market", http.StatusBadRequest)
return
}
pdfBytes, header, err := buildOrderPDFBytesForMail(mssql, orderID)
if err != nil {
http.Error(w, "pdf build error: "+err.Error(), http.StatusInternalServerError)
return
}
number := strings.TrimSpace(orderNo)
if number == "" && header != nil {
number = strings.TrimSpace(header.OrderNumber)
}
if number == "" {
number = orderID
}
marketLabel := strings.TrimSpace(marketTitle)
if marketLabel == "" {
marketLabel = strings.TrimSpace(marketCode)
}
subject := fmt.Sprintf("Sipariş %s - %s", number, marketLabel)
bodyHTML := fmt.Sprintf(
`<p>Sipariş kaydı oluşturuldu/güncellendi.</p><p><b>Sipariş No:</b> %s<br/><b>Cari:</b> %s<br/><b>Piyasa:</b> %s</p><p>PDF ektedir.</p>`,
htmlEsc(number),
htmlEsc(currAccCode),
htmlEsc(marketLabel),
)
fileNo := sanitizeFileName(number)
if fileNo == "" {
fileNo = orderID
}
msg := mailer.Message{
To: recipients,
Subject: subject,
BodyHTML: bodyHTML,
Attachments: []mailer.Attachment{
{
FileName: "ORDER_" + fileNo + ".pdf",
ContentType: "application/pdf",
Data: pdfBytes,
},
},
}
if err := ml.Send(context.Background(), msg); err != nil {
http.Error(w, "mail send error: "+err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(map[string]any{
"success": true,
"orderHeaderID": orderID,
"orderNumber": number,
"marketCode": marketCode,
"marketTitle": marketTitle,
"recipients": recipients,
"sentCount": len(recipients),
})
}
}
func resolveOrderMailContext(db *sql.DB, orderID string) (orderNo, currAccCode, marketCode, marketTitle string, err error) {
row := db.QueryRow(`
SELECT TOP (1)
ISNULL(h.OrderNumber, ''),
ISNULL(h.CurrAccCode, ''),
ISNULL(LTRIM(RTRIM(f.CustomerAtt01)), '') AS MarketCode,
ISNULL(py.AttributeDescription, '') AS MarketTitle
FROM BAGGI_V3.dbo.trOrderHeader h WITH (NOLOCK)
LEFT JOIN BAGGI_V3.dbo.CustomerAttributesFilter f WITH (NOLOCK)
ON f.CurrAccCode = h.CurrAccCode
LEFT JOIN BAGGI_V3.dbo.cdCurrAccAttributeDesc py WITH (NOLOCK)
ON py.CurrAccTypeCode = h.CurrAccTypeCode
AND py.AttributeTypeCode = 1
AND py.AttributeCode = f.CustomerAtt01
AND py.LangCode = 'TR'
WHERE CAST(h.OrderHeaderID AS varchar(36)) = @p1
`, orderID)
var no, cari, pCode, pTitle string
if err = row.Scan(&no, &cari, &pCode, &pTitle); err != nil {
return "", "", "", "", err
}
no = strings.TrimSpace(no)
cari = strings.TrimSpace(cari)
pCode = strings.TrimSpace(pCode)
pTitle = strings.TrimSpace(pTitle)
return no, cari, pCode, pTitle, nil
}
func loadMarketRecipients(pg *sql.DB, marketCode, marketTitle string) ([]string, error) {
rows, err := pg.Query(`
SELECT DISTINCT TRIM(m.email) AS email
FROM mk_sales_piy p
JOIN mk_market_mail mm
ON mm.market_id = p.id
JOIN mk_mail m
ON m.id = mm.mail_id
WHERE p.is_active = true
AND m.is_active = true
AND (
UPPER(TRIM(p.code)) = UPPER(TRIM($1))
OR ($2 <> '' AND UPPER(TRIM(p.title)) = UPPER(TRIM($2)))
)
AND COALESCE(TRIM(m.email), '') <> ''
ORDER BY email
`, strings.TrimSpace(marketCode), strings.TrimSpace(marketTitle))
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]string, 0, 8)
for rows.Next() {
var email string
if err := rows.Scan(&email); err != nil {
return nil, err
}
email = strings.TrimSpace(email)
if email != "" {
out = append(out, email)
}
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
}
func buildOrderPDFBytesForMail(db *sql.DB, orderID string) ([]byte, *OrderHeader, error) {
header, err := getOrderHeaderFromDB(db, orderID)
if err != nil {
return nil, nil, err
}
lines, err := getOrderLinesFromDB(db, orderID)
if err != nil {
return nil, nil, err
}
hasVat := false
var vatRate float64
for _, l := range lines {
if l.VatRate.Valid && l.VatRate.Float64 > 0 {
hasVat = true
vatRate = l.VatRate.Float64
break
}
}
rows := normalizeOrderLinesForPdf(lines)
pdf, err := newOrderPdf()
if err != nil {
return nil, nil, err
}
renderOrderGrid(pdf, header, rows, hasVat, vatRate)
if err := pdf.Error(); err != nil {
return nil, nil, err
}
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
return nil, nil, err
}
return buf.Bytes(), header, nil
}
func sanitizeFileName(v string) string {
s := strings.TrimSpace(v)
if s == "" {
return ""
}
invalid := []string{`\\`, `/`, `:`, `*`, `?`, `"`, `<`, `>`, `|`}
for _, bad := range invalid {
s = strings.ReplaceAll(s, bad, "_")
}
return strings.TrimSpace(s)
}
func htmlEsc(s string) string {
r := strings.NewReplacer(
"&", "&amp;",
"<", "&lt;",
">", "&gt;",
`"`, "&quot;",
"'", "&#39;",
)
return r.Replace(s)
}

View File

@@ -1,4 +1,4 @@
<template>
<template>
<q-page v-if="canReadFinance" class="q-px-md q-pb-md q-pt-xs page-col statement-page">
<q-slide-transition>
<div v-show="!filtersCollapsed" class="local-filter-bar compact-filter q-pa-sm q-mb-xs">
@@ -541,7 +541,6 @@ function formatAmount(value, fraction = 2) {
}
.currency-groups {
position: relative;
padding: 6px;
background: #f8faff;
}
@@ -630,20 +629,18 @@ function formatAmount(value, fraction = 2) {
line-height: 1.2 !important;
}
.detail-subtable :deep(.q-table__container),
.detail-subtable :deep(.q-table__middle) {
overflow: visible !important;
max-height: none !important;
}
.detail-subtable :deep(.q-table__top) {
position: static;
}
.detail-subtable :deep(thead th) {
.detail-subtable :deep(thead) {
position: sticky;
top: calc(var(--lvl3-top) - var(--lvl3-shift-up));
z-index: 35;
z-index: 31;
}
.detail-subtable :deep(thead th) {
position: static;
background: #1f3b5b;
color: #fff;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);

View File

@@ -400,6 +400,26 @@ export const useOrderEntryStore = defineStore('orderentry', {
}
,
async sendOrderToMarketMails(orderId) {
const id = String(orderId || this.header?.OrderHeaderID || '').trim()
if (!id) {
throw new Error('Sipariş ID bulunamadı')
}
try {
const res = await api.post('/order/send-market-mail', {
orderHeaderID: id
})
return res?.data || {}
} catch (err) {
const detail = await extractApiErrorDetail(err)
const status = err?.status || err?.response?.status || '-'
console.error(`❌ sendOrderToMarketMails hata [${status}] order=${id}: ${detail}`)
throw new Error(detail)
}
}
,
async downloadOrderPdf(id = null) {
try {
const orderId = id || this.header?.OrderHeaderID
@@ -2845,6 +2865,23 @@ export const useOrderEntryStore = defineStore('orderentry', {
productCache
})
// 📧 Piyasa eşleşen alıcılara sipariş PDF gönderimi (kayıt başarılı olduktan sonra)
try {
const mailRes = await this.sendOrderToMarketMails(serverOrderId)
const sentCount = Number(mailRes?.sentCount || 0)
$q.notify({
type: 'positive',
message: sentCount > 0
? `Sipariş PDF mail gönderildi (${sentCount} alıcı)`
: 'Sipariş PDF mail gönderimi tamamlandı'
})
} catch (mailErr) {
$q.notify({
type: 'warning',
message: `Sipariş kaydedildi, mail gönderilemedi: ${mailErr?.message || 'Bilinmeyen hata'}`
})
}
/* =======================================================
❓ USER NEXT STEP
======================================================= */