Merge remote-tracking branch 'origin/master'
This commit is contained in:
719
svc/routes/production_product_costing_mail.go
Normal file
719
svc/routes/production_product_costing_mail.go
Normal file
@@ -0,0 +1,719 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"bssapp-backend/internal/mailer"
|
||||
"bssapp-backend/models"
|
||||
"bssapp-backend/queries"
|
||||
)
|
||||
|
||||
type mailBucket struct {
|
||||
InputByCur map[string]float64
|
||||
USD float64
|
||||
TRY float64
|
||||
HasCMT bool
|
||||
// Fabric-only helpers (for UI parity)
|
||||
MeterQty float64
|
||||
MeterUom string
|
||||
UnitIn float64
|
||||
UnitCur string
|
||||
}
|
||||
|
||||
func formatDateTR2(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return "-"
|
||||
}
|
||||
// dd.MM.yyyy
|
||||
return t.Format("02.01.2006")
|
||||
}
|
||||
|
||||
func formatDateTimeTR2(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return "-"
|
||||
}
|
||||
// dd.MM.yyyy HH:mm
|
||||
return t.Format("02.01.2006 15:04")
|
||||
}
|
||||
|
||||
func formatAnyDateTimeTR2(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return "-"
|
||||
}
|
||||
// Common MSSQL string renderings (best-effort).
|
||||
layouts := []string{
|
||||
"2006-01-02 15:04:05.9999999",
|
||||
"2006-01-02 15:04:05.999999",
|
||||
"2006-01-02 15:04:05.999",
|
||||
"2006-01-02 15:04:05",
|
||||
time.RFC3339Nano,
|
||||
time.RFC3339,
|
||||
"2006-01-02T15:04:05",
|
||||
"2006-01-02",
|
||||
}
|
||||
for _, layout := range layouts {
|
||||
if t, err := time.Parse(layout, s); err == nil {
|
||||
// Date-only vs datetime
|
||||
if layout == "2006-01-02" {
|
||||
return formatDateTR2(t)
|
||||
}
|
||||
return formatDateTimeTR2(t)
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func addInputAmount(b *mailBucket, cur string, amount float64) {
|
||||
if math.IsNaN(amount) || math.IsInf(amount, 0) || amount == 0 {
|
||||
return
|
||||
}
|
||||
if b.InputByCur == nil {
|
||||
b.InputByCur = map[string]float64{}
|
||||
}
|
||||
b.InputByCur[cur] = b.InputByCur[cur] + amount
|
||||
}
|
||||
|
||||
func formatMoney2(v float64) string {
|
||||
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
v = 0
|
||||
}
|
||||
// Keep 2 decimals with dot (mail clients).
|
||||
return fmt.Sprintf("%.2f", v)
|
||||
}
|
||||
|
||||
func formatQty2(v float64) string {
|
||||
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
v = 0
|
||||
}
|
||||
return fmt.Sprintf("%.2f", v)
|
||||
}
|
||||
|
||||
func normalizePartFromMtBolumTitle(title string) string {
|
||||
v := strings.ToUpper(strings.TrimSpace(title))
|
||||
if v == "" {
|
||||
return ""
|
||||
}
|
||||
// Keep the part name dynamic (UI shows spUrtMTBolum.sAdi). Still normalize a few aliases.
|
||||
switch {
|
||||
case strings.Contains(v, "AKSESUAR") || strings.Contains(v, "AKS"):
|
||||
return "AKSESUAR"
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func summarizeInputByCurrency(b *mailBucket) (amountLabel string, curLabel string) {
|
||||
if b == nil || len(b.InputByCur) == 0 {
|
||||
return "-", "-"
|
||||
}
|
||||
curs := make([]string, 0, len(b.InputByCur))
|
||||
for c := range b.InputByCur {
|
||||
c = strings.ToUpper(strings.TrimSpace(c))
|
||||
if c == "" {
|
||||
continue
|
||||
}
|
||||
curs = append(curs, c)
|
||||
}
|
||||
sort.Strings(curs)
|
||||
if len(curs) == 0 {
|
||||
return "-", "-"
|
||||
}
|
||||
if len(curs) == 1 {
|
||||
c := curs[0]
|
||||
return formatMoney2(b.InputByCur[c]), c
|
||||
}
|
||||
sum := 0.0
|
||||
for _, c := range curs {
|
||||
sum += b.InputByCur[c]
|
||||
}
|
||||
return formatMoney2(sum), "MIX"
|
||||
}
|
||||
|
||||
func formatMeterLabel(b *mailBucket) string {
|
||||
if b == nil || !(b.MeterQty > 0) {
|
||||
return "-"
|
||||
}
|
||||
u := strings.TrimSpace(b.MeterUom)
|
||||
if u == "" {
|
||||
u = "MT"
|
||||
}
|
||||
return fmt.Sprintf("%s %s", formatQty2(b.MeterQty), u)
|
||||
}
|
||||
|
||||
func loadCostingRecipients(pg *sql.DB, firstGroupCode string) ([]string, error) {
|
||||
rows, err := pg.Query(`
|
||||
SELECT DISTINCT TRIM(m.email) AS email
|
||||
FROM mk_costing_first_group_mail f
|
||||
JOIN mk_mail m
|
||||
ON m.id = f.mail_id
|
||||
WHERE m.is_active = true
|
||||
AND COALESCE(TRIM(m.email), '') <> ''
|
||||
AND UPPER(TRIM(f.urun_ilk_grubu)) = UPPER(TRIM($1))
|
||||
ORDER BY email
|
||||
`, strings.TrimSpace(firstGroupCode))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := make([]string, 0, 16)
|
||||
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)
|
||||
}
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func sendCostingSummaryMail(
|
||||
ctx context.Context,
|
||||
pg *sql.DB,
|
||||
mssql *sql.DB,
|
||||
uretim *sql.DB,
|
||||
ml *mailer.GraphMailer,
|
||||
req models.ProductionProductCostingOnMLSaveRequest,
|
||||
nOnMLNo int,
|
||||
isUpdate bool,
|
||||
usdRate float64,
|
||||
eurRate float64,
|
||||
gbpRate float64,
|
||||
totalUSD float64,
|
||||
totalTRY float64,
|
||||
totalEUR float64,
|
||||
actor string,
|
||||
) error {
|
||||
if ml == nil {
|
||||
return fmt.Errorf("mailer not initialized")
|
||||
}
|
||||
if pg == nil || mssql == nil {
|
||||
return fmt.Errorf("db not initialized")
|
||||
}
|
||||
|
||||
// Ensure mapping tables exist (first save can happen before mapping screens are visited).
|
||||
if err := ensureFirstGroupMailMappingTables(pg); err != nil {
|
||||
return fmt.Errorf("mapping table bootstrap error: %w", err)
|
||||
}
|
||||
|
||||
firstGroupCode, _, err := queries.GetProductFirstGroupCodeDescByUrunKodu(ctx, mssql, req.Header.UrunKodu)
|
||||
if err != nil {
|
||||
return fmt.Errorf("first group resolve error: %w", err)
|
||||
}
|
||||
if strings.TrimSpace(firstGroupCode) == "" {
|
||||
return fmt.Errorf("first group code not found for product")
|
||||
}
|
||||
|
||||
recipients, err := loadCostingRecipients(pg, firstGroupCode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("recipient query error: %w", err)
|
||||
}
|
||||
if len(recipients) == 0 {
|
||||
// Don't hard fail; mapping might be intentionally empty.
|
||||
return fmt.Errorf("no costing mail mapping for first group: %s", firstGroupCode)
|
||||
}
|
||||
|
||||
// Pull the same header payload used by UI (best-effort) so the mail can show every header label.
|
||||
var uiHeader models.ProductionHasCostDetailHeader
|
||||
uiHeaderLoaded := false
|
||||
if uretim != nil && nOnMLNo > 0 {
|
||||
row, err := queries.GetProductionHasCostDetailHeaderByOnMLNo(ctx, uretim, nOnMLNo)
|
||||
if err == nil && row != nil {
|
||||
// Keep scan fields aligned with GetProductionHasCostDetailHeaderHandler
|
||||
if err := row.Scan(
|
||||
&uiHeader.UretimiYapanFirma,
|
||||
&uiHeader.SonIsEmriVeren,
|
||||
&uiHeader.FirmaKodu,
|
||||
&uiHeader.NFirmaID,
|
||||
&uiHeader.NOnMLNo,
|
||||
&uiHeader.UrunKodu,
|
||||
&uiHeader.UrunAdi,
|
||||
&uiHeader.UretimSekliID,
|
||||
&uiHeader.UretimSekli,
|
||||
&uiHeader.MaliyetTarihi,
|
||||
&uiHeader.DteKayitTarihi,
|
||||
&uiHeader.SKullaniciAdi,
|
||||
&uiHeader.LTutarTL,
|
||||
&uiHeader.LTutarUSD,
|
||||
&uiHeader.LTutarEURO,
|
||||
&uiHeader.LTutarGBP,
|
||||
&uiHeader.SDovizCinsi,
|
||||
&uiHeader.LTutarDoviz,
|
||||
&uiHeader.DteGuncellemeTarihi,
|
||||
&uiHeader.SGuncellemeKullaniciAdi,
|
||||
&uiHeader.NUrtReceteID,
|
||||
); err == nil {
|
||||
uiHeaderLoaded = true
|
||||
if mssql != nil {
|
||||
ilk, ana, alt, _ := queries.GetProductIlkAnaAltGrupByUrunKodu(ctx, mssql, uiHeader.UrunKodu)
|
||||
uiHeader.UrunIlkGrubu = ilk
|
||||
uiHeader.UrunAnaGrubu = ana
|
||||
uiHeader.UrunAltGrubu = alt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enrich header (fallback) if UI header wasn't loadable.
|
||||
ilkGrup, anaGrup, altGrup := "", "", ""
|
||||
if uiHeaderLoaded {
|
||||
ilkGrup, anaGrup, altGrup = strings.TrimSpace(uiHeader.UrunIlkGrubu), strings.TrimSpace(uiHeader.UrunAnaGrubu), strings.TrimSpace(uiHeader.UrunAltGrubu)
|
||||
} else if mssql != nil {
|
||||
ilkGrup, anaGrup, altGrup, _ = queries.GetProductIlkAnaAltGrupByUrunKodu(ctx, mssql, req.Header.UrunKodu)
|
||||
}
|
||||
|
||||
// Resolve MT bolum titles so we can bucket rows into CEKET/PANTOLON/YELEK/AKSESUAR.
|
||||
mtTitleByID := map[int]string{}
|
||||
if uretim != nil {
|
||||
ids := make([]int, 0, len(req.Detail.Upserts))
|
||||
seen := map[int]struct{}{}
|
||||
for _, row := range req.Detail.Upserts {
|
||||
if row.NUrtMTBolumID <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[row.NUrtMTBolumID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[row.NUrtMTBolumID] = struct{}{}
|
||||
ids = append(ids, row.NUrtMTBolumID)
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
vals := make([]string, 0, len(ids))
|
||||
args := make([]any, 0, len(ids))
|
||||
for i, id := range ids {
|
||||
vals = append(vals, fmt.Sprintf("(@p%d)", i+1))
|
||||
args = append(args, id)
|
||||
}
|
||||
q := fmt.Sprintf(`
|
||||
WITH X AS (SELECT CONVERT(int, V.id) AS id FROM (VALUES %s) AS V(id))
|
||||
SELECT X.id, LTRIM(RTRIM(ISNULL(M.sAdi,''))) AS title
|
||||
FROM X
|
||||
LEFT JOIN dbo.spUrtMTBolum M WITH (NOLOCK)
|
||||
ON M.nUrtMTBolumID = X.id
|
||||
`, strings.Join(vals, ","))
|
||||
rows, err := uretim.QueryContext(ctx, q, args...)
|
||||
if err == nil {
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var title string
|
||||
if err := rows.Scan(&id, &title); err != nil {
|
||||
continue
|
||||
}
|
||||
mtTitleByID[id] = strings.TrimSpace(title)
|
||||
}
|
||||
_ = rows.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic part list derived from detail rows.
|
||||
preferred := []string{"CEKET", "PANTOLON", "YELEK", "AKSESUAR", "YAKA"}
|
||||
seen := map[string]struct{}{}
|
||||
dynamic := make([]string, 0, 16)
|
||||
|
||||
for _, row := range req.Detail.Upserts {
|
||||
group := strings.ToUpper(strings.TrimSpace(row.SAciklama3))
|
||||
included := strings.Contains(group, "CM2") || strings.Contains(group, "CM1") || row.MaliyeteDahil == 1
|
||||
if !included {
|
||||
continue
|
||||
}
|
||||
part := ""
|
||||
if t := mtTitleByID[row.NUrtMTBolumID]; t != "" {
|
||||
part = normalizePartFromMtBolumTitle(t)
|
||||
}
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[part]; ok {
|
||||
continue
|
||||
}
|
||||
seen[part] = struct{}{}
|
||||
dynamic = append(dynamic, part)
|
||||
}
|
||||
|
||||
parts := make([]string, 0, len(dynamic)+len(preferred))
|
||||
for _, p := range preferred {
|
||||
if _, ok := seen[p]; ok {
|
||||
parts = append(parts, p)
|
||||
}
|
||||
}
|
||||
for _, p := range dynamic {
|
||||
isPreferred := false
|
||||
for _, pref := range preferred {
|
||||
if pref == p {
|
||||
isPreferred = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isPreferred {
|
||||
parts = append(parts, p)
|
||||
}
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
parts = []string{"TANIMSIZ"}
|
||||
}
|
||||
labor := map[string]*mailBucket{}
|
||||
material := map[string]*mailBucket{}
|
||||
fabric := map[string]*mailBucket{}
|
||||
for _, p := range parts {
|
||||
labor[p] = &mailBucket{}
|
||||
material[p] = &mailBucket{}
|
||||
fabric[p] = &mailBucket{}
|
||||
}
|
||||
|
||||
for _, row := range req.Detail.Upserts {
|
||||
group := strings.ToUpper(strings.TrimSpace(row.SAciklama3))
|
||||
cur := strings.ToUpper(strings.TrimSpace(row.FiyatDoviz))
|
||||
qty := row.LMiktar
|
||||
if qty < 0 {
|
||||
qty = 0
|
||||
}
|
||||
in := row.FiyatGirilen
|
||||
|
||||
// Included rule: CM2 always; others only when maliyete_dahil = 1.
|
||||
included := strings.Contains(group, "CM2") || strings.Contains(group, "CM1") || row.MaliyeteDahil == 1
|
||||
if !included {
|
||||
continue
|
||||
}
|
||||
|
||||
// Part bucket:
|
||||
part := ""
|
||||
if t := mtTitleByID[row.NUrtMTBolumID]; t != "" {
|
||||
part = normalizePartFromMtBolumTitle(t)
|
||||
}
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert input to TRY unit
|
||||
unitTRY := in
|
||||
switch cur {
|
||||
case "USD":
|
||||
unitTRY = in * usdRate
|
||||
case "EUR":
|
||||
unitTRY = in * eurRate
|
||||
case "GBP":
|
||||
unitTRY = in * gbpRate
|
||||
case "TRY", "TL", "":
|
||||
unitTRY = in
|
||||
default:
|
||||
unitTRY = in
|
||||
}
|
||||
unitUSD := 0.0
|
||||
if usdRate > 0 {
|
||||
unitUSD = unitTRY / usdRate
|
||||
}
|
||||
amountTRY := unitTRY * qty
|
||||
amountUSD := unitUSD * qty
|
||||
|
||||
// input totals for display: inputPrice * qty in input currency
|
||||
inputAmount := in * qty
|
||||
|
||||
if strings.Contains(group, "CM2") || strings.Contains(group, "CM1") {
|
||||
b := labor[part]
|
||||
b.USD += amountUSD
|
||||
b.TRY += amountTRY
|
||||
addInputAmount(b, cur, inputAmount)
|
||||
// UI rule: tick only when cm_price_type_id == 2 (malzemeli). Nil/empty defaults to 1 (unticked).
|
||||
if row.CMPriceTypeID != nil && *row.CMPriceTypeID == 2 {
|
||||
b.HasCMT = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if group == "DT" || strings.Contains(group, " DT") || group == "TP" || strings.Contains(group, " TP") {
|
||||
b := material[part]
|
||||
b.USD += amountUSD
|
||||
b.TRY += amountTRY
|
||||
addInputAmount(b, cur, inputAmount)
|
||||
continue
|
||||
}
|
||||
if group == "FABRIC" || strings.Contains(group, "FABRIC") {
|
||||
b := fabric[part]
|
||||
b.USD += amountUSD
|
||||
b.TRY += amountTRY
|
||||
addInputAmount(b, cur, inputAmount)
|
||||
// UI parity: fabric summary shows metraj and a representative unit input price (first non-zero).
|
||||
if qty > 0 {
|
||||
b.MeterQty += qty
|
||||
if strings.TrimSpace(b.MeterUom) == "" {
|
||||
if u := strings.TrimSpace(row.SBirim); u != "" {
|
||||
b.MeterUom = u
|
||||
}
|
||||
}
|
||||
}
|
||||
if b.UnitIn <= 0 && in > 0 {
|
||||
b.UnitIn = in
|
||||
b.UnitCur = cur
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maliyetTarihi := strings.TrimSpace(req.Header.MaliyetTarihi)
|
||||
if uiHeaderLoaded {
|
||||
// Prefer the UI header date (can differ from "today").
|
||||
if v := strings.TrimSpace(uiHeader.MaliyetTarihi); v != "" {
|
||||
maliyetTarihi = v
|
||||
}
|
||||
}
|
||||
// Display format: dd.MM.yyyy (mail). Keep the original YYYY-MM-DD for subject readability if present.
|
||||
maliyetTarihiLabel := maliyetTarihi
|
||||
if parsed, err := time.Parse("2006-01-02", maliyetTarihi); err == nil {
|
||||
maliyetTarihiLabel = formatDateTR2(parsed)
|
||||
} else if maliyetTarihi == "" {
|
||||
maliyetTarihi = time.Now().Format("2006-01-02")
|
||||
maliyetTarihiLabel = formatDateTR2(time.Now())
|
||||
}
|
||||
|
||||
titleLabel := "MALIYETI GIRIS YAPILAN URUN"
|
||||
if isUpdate {
|
||||
titleLabel = "MALIYETI GUNCELLENEN URUN"
|
||||
}
|
||||
subject := fmt.Sprintf("%s | %s | %s | OnML:%d", strings.TrimSpace(req.Header.UrunKodu), titleLabel, maliyetTarihi, nOnMLNo)
|
||||
if strings.TrimSpace(actor) != "" {
|
||||
subject = fmt.Sprintf("%s tarafindan %s", strings.TrimSpace(actor), subject)
|
||||
}
|
||||
|
||||
// Build HTML with 4 tables.
|
||||
var b strings.Builder
|
||||
b.WriteString(`<div style="font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#1f2a37;">`)
|
||||
b.WriteString(fmt.Sprintf(`<h3 style="margin:8px 0 6px;font-size:13px;">%s</h3>`, htmlEsc(titleLabel)))
|
||||
b.WriteString(`<table cellpadding="6" cellspacing="0" style="border-collapse:collapse;border:1px solid #e5e7eb;min-width:720px;">`)
|
||||
b.WriteString(`<tr style="background:#f3f4f6;font-weight:bold;"><td style="width:220px;">Alan</td><td>Deger</td></tr>`)
|
||||
|
||||
// Prefer resolved UI header (more complete), fallback to request header.
|
||||
urunKodu := strings.TrimSpace(req.Header.UrunKodu)
|
||||
urunAdi := strings.TrimSpace(req.Header.UrunAdi)
|
||||
if uiHeaderLoaded {
|
||||
if v := strings.TrimSpace(uiHeader.UrunKodu); v != "" {
|
||||
urunKodu = v
|
||||
}
|
||||
if v := strings.TrimSpace(uiHeader.UrunAdi); v != "" {
|
||||
urunAdi = v
|
||||
}
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>UrunKodu</td><td>%s</td></tr>`, htmlEsc(urunKodu)))
|
||||
if urunAdi != "" {
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>UrunAdi</td><td>%s</td></tr>`, htmlEsc(urunAdi)))
|
||||
}
|
||||
if strings.TrimSpace(ilkGrup) != "" || strings.TrimSpace(anaGrup) != "" || strings.TrimSpace(altGrup) != "" {
|
||||
if strings.TrimSpace(ilkGrup) != "" {
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>Urun Ilk Grubu</td><td>%s</td></tr>`, htmlEsc(ilkGrup)))
|
||||
}
|
||||
if strings.TrimSpace(anaGrup) != "" {
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>Urun Ana Grubu</td><td>%s</td></tr>`, htmlEsc(anaGrup)))
|
||||
}
|
||||
if strings.TrimSpace(altGrup) != "" {
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>Urun Alt Grubu</td><td>%s</td></tr>`, htmlEsc(altGrup)))
|
||||
}
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>Maliyet Tarihi</td><td>%s</td></tr>`, htmlEsc(maliyetTarihiLabel)))
|
||||
|
||||
// Mirror UI header labels when available:
|
||||
if uiHeaderLoaded {
|
||||
if strings.TrimSpace(uiHeader.UretimSekli) != "" {
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>Uretim Sekli</td><td>%s</td></tr>`, htmlEsc(uiHeader.UretimSekli)))
|
||||
} else if strings.TrimSpace(uiHeader.UretimSekliID) != "" {
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>Uretim Sekli ID</td><td>%s</td></tr>`, htmlEsc(uiHeader.UretimSekliID)))
|
||||
}
|
||||
if strings.TrimSpace(uiHeader.UretimiYapanFirma) != "" {
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>Uretimi Yapan Firma</td><td>%s</td></tr>`, htmlEsc(uiHeader.UretimiYapanFirma)))
|
||||
}
|
||||
if strings.TrimSpace(uiHeader.SonIsEmriVeren) != "" {
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>2.Firma</td><td>%s</td></tr>`, htmlEsc(uiHeader.SonIsEmriVeren)))
|
||||
}
|
||||
if strings.TrimSpace(uiHeader.NOnMLNo) != "" {
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>nOnMLNo</td><td>%s</td></tr>`, htmlEsc(uiHeader.NOnMLNo)))
|
||||
} else {
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>nOnMLNo</td><td>%d</td></tr>`, nOnMLNo))
|
||||
}
|
||||
if strings.TrimSpace(uiHeader.SKullaniciAdi) != "" {
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>sKullaniciAdi</td><td>%s</td></tr>`, htmlEsc(uiHeader.SKullaniciAdi)))
|
||||
}
|
||||
if strings.TrimSpace(uiHeader.DteKayitTarihi) != "" {
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>Kayit Tarihi</td><td>%s</td></tr>`, htmlEsc(formatAnyDateTimeTR2(uiHeader.DteKayitTarihi))))
|
||||
}
|
||||
if strings.TrimSpace(uiHeader.DteGuncellemeTarihi) != "" {
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>Son Guncelleme Tarihi</td><td>%s</td></tr>`, htmlEsc(formatAnyDateTimeTR2(uiHeader.DteGuncellemeTarihi))))
|
||||
}
|
||||
if strings.TrimSpace(uiHeader.SGuncellemeKullaniciAdi) != "" {
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>sGuncellemeKullaniciAdi</td><td>%s</td></tr>`, htmlEsc(uiHeader.SGuncellemeKullaniciAdi)))
|
||||
}
|
||||
if strings.TrimSpace(uiHeader.NUrtReceteID) != "" {
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>nUrtReceteID</td><td>%s</td></tr>`, htmlEsc(uiHeader.NUrtReceteID)))
|
||||
}
|
||||
} else {
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>nOnMLNo</td><td>%d</td></tr>`, nOnMLNo))
|
||||
if req.Header.NUrtReceteID > 0 {
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>nUrtReceteID</td><td>%d</td></tr>`, req.Header.NUrtReceteID))
|
||||
}
|
||||
if req.Header.UretimSekliID > 0 {
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>Uretim Sekli ID</td><td>%d</td></tr>`, req.Header.UretimSekliID))
|
||||
}
|
||||
}
|
||||
|
||||
// Free text description (from request).
|
||||
if strings.TrimSpace(req.Header.SAciklama) != "" {
|
||||
b.WriteString(fmt.Sprintf(`<tr><td>Aciklama</td><td>%s</td></tr>`, htmlEsc(req.Header.SAciklama)))
|
||||
}
|
||||
|
||||
b.WriteString(`</table>`)
|
||||
|
||||
// 1) Header totals
|
||||
b.WriteString(`<h3 style="margin:12px 0 6px;font-size:13px;">Maliyetlere Islenen Toplam Tutar</h3>`)
|
||||
b.WriteString(`<table cellpadding="6" cellspacing="0" style="border-collapse:collapse;border:1px solid #e5e7eb;min-width:520px;">`)
|
||||
gbpTotal := 0.0
|
||||
if gbpRate > 0 {
|
||||
gbpTotal = totalTRY / gbpRate
|
||||
}
|
||||
// UI format (2 rows, key/value pairs)
|
||||
b.WriteString(fmt.Sprintf(`<tr><td style="font-weight:bold;color:#374151;">USD</td><td style="text-align:right;">%s</td><td style="font-weight:bold;color:#374151;">TRY</td><td style="text-align:right;">%s</td></tr>`,
|
||||
formatMoney2(totalUSD), formatMoney2(totalTRY)))
|
||||
b.WriteString(fmt.Sprintf(`<tr><td style="font-weight:bold;color:#374151;">EUR</td><td style="text-align:right;">%s</td><td style="font-weight:bold;color:#374151;">GBP</td><td style="text-align:right;">%s</td></tr>`,
|
||||
formatMoney2(totalEUR), formatMoney2(gbpTotal)))
|
||||
b.WriteString(`</table>`)
|
||||
|
||||
renderLaborTable := func(title string, m map[string]*mailBucket) {
|
||||
b.WriteString(fmt.Sprintf(`<h3 style="margin:14px 0 6px;font-size:13px;">%s</h3>`, htmlEsc(title)))
|
||||
b.WriteString(`<table cellpadding="6" cellspacing="0" style="border-collapse:collapse;border:1px solid #e5e7eb;min-width:720px;">`)
|
||||
b.WriteString(`<tr style="background:#f3f4f6;font-weight:bold;">`)
|
||||
b.WriteString(`<td>Parca</td><td style="text-align:right;">Giris</td><td>Pr.Br.</td><td style="text-align:right;">USD Tutar</td><td style="text-align:right;">TRY Tutar</td><td style="text-align:center;">CMT/Malzemeli</td>`)
|
||||
b.WriteString(`</tr>`)
|
||||
totalUSD := 0.0
|
||||
totalTRY := 0.0
|
||||
for _, p := range parts {
|
||||
row := m[p]
|
||||
inAmt, inCur := summarizeInputByCurrency(row)
|
||||
tick := ""
|
||||
if row != nil && row.HasCMT {
|
||||
tick = "✓"
|
||||
}
|
||||
if row != nil {
|
||||
totalUSD += row.USD
|
||||
totalTRY += row.TRY
|
||||
}
|
||||
b.WriteString(`<tr>`)
|
||||
b.WriteString(fmt.Sprintf(`<td>%s</td>`, htmlEsc(p)))
|
||||
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, htmlEsc(inAmt)))
|
||||
b.WriteString(fmt.Sprintf(`<td>%s</td>`, htmlEsc(inCur)))
|
||||
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(row.USD)))
|
||||
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(row.TRY)))
|
||||
b.WriteString(fmt.Sprintf(`<td style="text-align:center;">%s</td>`, tick))
|
||||
b.WriteString(`</tr>`)
|
||||
}
|
||||
b.WriteString(`<tr style="border-top:2px solid #e5e7eb;font-weight:bold;background:#fbfbfd;">`)
|
||||
b.WriteString(`<td>TOPLAM</td><td></td><td></td>`)
|
||||
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(totalUSD)))
|
||||
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(totalTRY)))
|
||||
b.WriteString(`<td></td>`)
|
||||
b.WriteString(`</tr>`)
|
||||
b.WriteString(`</table>`)
|
||||
}
|
||||
|
||||
renderMaterialTable := func(title string, m map[string]*mailBucket) {
|
||||
b.WriteString(fmt.Sprintf(`<h3 style="margin:14px 0 6px;font-size:13px;">%s</h3>`, htmlEsc(title)))
|
||||
b.WriteString(`<table cellpadding="6" cellspacing="0" style="border-collapse:collapse;border:1px solid #e5e7eb;min-width:520px;">`)
|
||||
b.WriteString(`<tr style="background:#f3f4f6;font-weight:bold;"><td>Parca</td><td style="text-align:right;">USD Tutar</td><td style="text-align:right;">TRY Tutar</td></tr>`)
|
||||
totalUSD := 0.0
|
||||
totalTRY := 0.0
|
||||
for _, p := range parts {
|
||||
row := m[p]
|
||||
if row != nil {
|
||||
totalUSD += row.USD
|
||||
totalTRY += row.TRY
|
||||
}
|
||||
b.WriteString(`<tr>`)
|
||||
b.WriteString(fmt.Sprintf(`<td>%s</td>`, htmlEsc(p)))
|
||||
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(row.USD)))
|
||||
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(row.TRY)))
|
||||
b.WriteString(`</tr>`)
|
||||
}
|
||||
b.WriteString(`<tr style="border-top:2px solid #e5e7eb;font-weight:bold;background:#fbfbfd;">`)
|
||||
b.WriteString(`<td>TOPLAM</td>`)
|
||||
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(totalUSD)))
|
||||
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(totalTRY)))
|
||||
b.WriteString(`</tr>`)
|
||||
b.WriteString(`</table>`)
|
||||
}
|
||||
|
||||
renderFabricTable := func(title string, m map[string]*mailBucket) {
|
||||
b.WriteString(fmt.Sprintf(`<h3 style="margin:14px 0 6px;font-size:13px;">%s</h3>`, htmlEsc(title)))
|
||||
b.WriteString(`<table cellpadding="6" cellspacing="0" style="border-collapse:collapse;border:1px solid #e5e7eb;min-width:720px;">`)
|
||||
b.WriteString(`<tr style="background:#f3f4f6;font-weight:bold;">`)
|
||||
b.WriteString(`<td>Parca</td><td style="text-align:right;">Metraj</td><td style="text-align:right;">MT Giris Fiyat</td><td>Pr.Br.</td><td style="text-align:right;">USD Tutar</td><td style="text-align:right;">TRY Tutar</td>`)
|
||||
b.WriteString(`</tr>`)
|
||||
totalUSD := 0.0
|
||||
totalTRY := 0.0
|
||||
totalMeter := 0.0
|
||||
for _, p := range parts {
|
||||
row := m[p]
|
||||
inAmt, inCur := summarizeInputByCurrency(row)
|
||||
_ = inAmt
|
||||
unitLabel := "-"
|
||||
curLabel := inCur
|
||||
if row != nil && row.UnitIn > 0 {
|
||||
unitLabel = formatMoney2(row.UnitIn)
|
||||
if strings.TrimSpace(row.UnitCur) != "" {
|
||||
curLabel = strings.ToUpper(strings.TrimSpace(row.UnitCur))
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(curLabel) == "" {
|
||||
curLabel = "-"
|
||||
}
|
||||
if row != nil {
|
||||
totalUSD += row.USD
|
||||
totalTRY += row.TRY
|
||||
totalMeter += row.MeterQty
|
||||
}
|
||||
b.WriteString(`<tr>`)
|
||||
b.WriteString(fmt.Sprintf(`<td>%s</td>`, htmlEsc(p)))
|
||||
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, htmlEsc(formatMeterLabel(row))))
|
||||
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, htmlEsc(unitLabel)))
|
||||
b.WriteString(fmt.Sprintf(`<td>%s</td>`, htmlEsc(curLabel)))
|
||||
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(row.USD)))
|
||||
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(row.TRY)))
|
||||
b.WriteString(`</tr>`)
|
||||
}
|
||||
totalMeterLabel := "-"
|
||||
if totalMeter > 0 {
|
||||
totalMeterLabel = fmt.Sprintf("%s MT", formatQty2(totalMeter))
|
||||
}
|
||||
b.WriteString(`<tr style="border-top:2px solid #e5e7eb;font-weight:bold;background:#fbfbfd;">`)
|
||||
b.WriteString(`<td>TOPLAM</td>`)
|
||||
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, htmlEsc(totalMeterLabel)))
|
||||
b.WriteString(`<td></td><td></td>`)
|
||||
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(totalUSD)))
|
||||
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(totalTRY)))
|
||||
b.WriteString(`</tr>`)
|
||||
b.WriteString(`</table>`)
|
||||
}
|
||||
|
||||
renderLaborTable("Iscilik Fiyatlari (CM2)", labor)
|
||||
renderMaterialTable("Malzeme Fiyatlari (DT/TP, maliyete dahil)", material)
|
||||
renderFabricTable("Kumas Fiyatlari (FABRIC, maliyete dahil)", fabric)
|
||||
|
||||
b.WriteString(`<p style="margin-top:10px;color:#6b7280;"><i>Bu mail BaggiSS App uzerinden otomatik gonderilmistir.</i></p>`)
|
||||
b.WriteString(`</div>`)
|
||||
|
||||
msg := mailer.Message{
|
||||
To: recipients,
|
||||
Subject: subject,
|
||||
BodyHTML: b.String(),
|
||||
}
|
||||
|
||||
// Graph send
|
||||
if err := ml.Send(context.Background(), msg); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user