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(`
`)
b.WriteString(fmt.Sprintf(`
%s
`, htmlEsc(titleLabel)))
b.WriteString(`
`)
b.WriteString(`| Alan | Deger |
`)
// 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(`| UrunKodu | %s |
`, htmlEsc(urunKodu)))
if urunAdi != "" {
b.WriteString(fmt.Sprintf(`| UrunAdi | %s |
`, htmlEsc(urunAdi)))
}
if strings.TrimSpace(ilkGrup) != "" || strings.TrimSpace(anaGrup) != "" || strings.TrimSpace(altGrup) != "" {
if strings.TrimSpace(ilkGrup) != "" {
b.WriteString(fmt.Sprintf(`| Urun Ilk Grubu | %s |
`, htmlEsc(ilkGrup)))
}
if strings.TrimSpace(anaGrup) != "" {
b.WriteString(fmt.Sprintf(`| Urun Ana Grubu | %s |
`, htmlEsc(anaGrup)))
}
if strings.TrimSpace(altGrup) != "" {
b.WriteString(fmt.Sprintf(`| Urun Alt Grubu | %s |
`, htmlEsc(altGrup)))
}
}
b.WriteString(fmt.Sprintf(`| Maliyet Tarihi | %s |
`, htmlEsc(maliyetTarihiLabel)))
// Mirror UI header labels when available:
if uiHeaderLoaded {
if strings.TrimSpace(uiHeader.UretimSekli) != "" {
b.WriteString(fmt.Sprintf(`| Uretim Sekli | %s |
`, htmlEsc(uiHeader.UretimSekli)))
} else if strings.TrimSpace(uiHeader.UretimSekliID) != "" {
b.WriteString(fmt.Sprintf(`| Uretim Sekli ID | %s |
`, htmlEsc(uiHeader.UretimSekliID)))
}
if strings.TrimSpace(uiHeader.UretimiYapanFirma) != "" {
b.WriteString(fmt.Sprintf(`| Uretimi Yapan Firma | %s |
`, htmlEsc(uiHeader.UretimiYapanFirma)))
}
if strings.TrimSpace(uiHeader.SonIsEmriVeren) != "" {
b.WriteString(fmt.Sprintf(`| 2.Firma | %s |
`, htmlEsc(uiHeader.SonIsEmriVeren)))
}
if strings.TrimSpace(uiHeader.NOnMLNo) != "" {
b.WriteString(fmt.Sprintf(`| nOnMLNo | %s |
`, htmlEsc(uiHeader.NOnMLNo)))
} else {
b.WriteString(fmt.Sprintf(`| nOnMLNo | %d |
`, nOnMLNo))
}
if strings.TrimSpace(uiHeader.SKullaniciAdi) != "" {
b.WriteString(fmt.Sprintf(`| sKullaniciAdi | %s |
`, htmlEsc(uiHeader.SKullaniciAdi)))
}
if strings.TrimSpace(uiHeader.DteKayitTarihi) != "" {
b.WriteString(fmt.Sprintf(`| Kayit Tarihi | %s |
`, htmlEsc(formatAnyDateTimeTR2(uiHeader.DteKayitTarihi))))
}
if strings.TrimSpace(uiHeader.DteGuncellemeTarihi) != "" {
b.WriteString(fmt.Sprintf(`| Son Guncelleme Tarihi | %s |
`, htmlEsc(formatAnyDateTimeTR2(uiHeader.DteGuncellemeTarihi))))
}
if strings.TrimSpace(uiHeader.SGuncellemeKullaniciAdi) != "" {
b.WriteString(fmt.Sprintf(`| sGuncellemeKullaniciAdi | %s |
`, htmlEsc(uiHeader.SGuncellemeKullaniciAdi)))
}
if strings.TrimSpace(uiHeader.NUrtReceteID) != "" {
b.WriteString(fmt.Sprintf(`| nUrtReceteID | %s |
`, htmlEsc(uiHeader.NUrtReceteID)))
}
} else {
b.WriteString(fmt.Sprintf(`| nOnMLNo | %d |
`, nOnMLNo))
if req.Header.NUrtReceteID > 0 {
b.WriteString(fmt.Sprintf(`| nUrtReceteID | %d |
`, req.Header.NUrtReceteID))
}
if req.Header.UretimSekliID > 0 {
b.WriteString(fmt.Sprintf(`| Uretim Sekli ID | %d |
`, req.Header.UretimSekliID))
}
}
// Free text description (from request).
if strings.TrimSpace(req.Header.SAciklama) != "" {
b.WriteString(fmt.Sprintf(`| Aciklama | %s |
`, htmlEsc(req.Header.SAciklama)))
}
b.WriteString(`
`)
// 1) Header totals
b.WriteString(`
Maliyetlere Islenen Toplam Tutar
`)
b.WriteString(`
`)
gbpTotal := 0.0
if gbpRate > 0 {
gbpTotal = totalTRY / gbpRate
}
// UI format (2 rows, key/value pairs)
b.WriteString(fmt.Sprintf(`| USD | %s | TRY | %s |
`,
formatMoney2(totalUSD), formatMoney2(totalTRY)))
b.WriteString(fmt.Sprintf(`| EUR | %s | GBP | %s |
`,
formatMoney2(totalEUR), formatMoney2(gbpTotal)))
b.WriteString(`
`)
renderLaborTable := func(title string, m map[string]*mailBucket) {
b.WriteString(fmt.Sprintf(`
%s
`, htmlEsc(title)))
b.WriteString(`
`)
b.WriteString(``)
b.WriteString(`| Parca | Giris | Pr.Br. | USD Tutar | TRY Tutar | CMT/Malzemeli | `)
b.WriteString(`
`)
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(``)
b.WriteString(fmt.Sprintf(`| %s | `, htmlEsc(p)))
b.WriteString(fmt.Sprintf(`%s | `, htmlEsc(inAmt)))
b.WriteString(fmt.Sprintf(`%s | `, htmlEsc(inCur)))
b.WriteString(fmt.Sprintf(`%s | `, formatMoney2(row.USD)))
b.WriteString(fmt.Sprintf(`%s | `, formatMoney2(row.TRY)))
b.WriteString(fmt.Sprintf(`%s | `, tick))
b.WriteString(`
`)
}
b.WriteString(``)
b.WriteString(`| TOPLAM | | | `)
b.WriteString(fmt.Sprintf(`%s | `, formatMoney2(totalUSD)))
b.WriteString(fmt.Sprintf(`%s | `, formatMoney2(totalTRY)))
b.WriteString(` | `)
b.WriteString(`
`)
b.WriteString(`
`)
}
renderMaterialTable := func(title string, m map[string]*mailBucket) {
b.WriteString(fmt.Sprintf(`
%s
`, htmlEsc(title)))
b.WriteString(`
`)
b.WriteString(`| Parca | USD Tutar | TRY Tutar |
`)
totalUSD := 0.0
totalTRY := 0.0
for _, p := range parts {
row := m[p]
if row != nil {
totalUSD += row.USD
totalTRY += row.TRY
}
b.WriteString(``)
b.WriteString(fmt.Sprintf(`| %s | `, htmlEsc(p)))
b.WriteString(fmt.Sprintf(`%s | `, formatMoney2(row.USD)))
b.WriteString(fmt.Sprintf(`%s | `, formatMoney2(row.TRY)))
b.WriteString(`
`)
}
b.WriteString(``)
b.WriteString(`| TOPLAM | `)
b.WriteString(fmt.Sprintf(`%s | `, formatMoney2(totalUSD)))
b.WriteString(fmt.Sprintf(`%s | `, formatMoney2(totalTRY)))
b.WriteString(`
`)
b.WriteString(`
`)
}
renderFabricTable := func(title string, m map[string]*mailBucket) {
b.WriteString(fmt.Sprintf(`
%s
`, htmlEsc(title)))
b.WriteString(`
`)
b.WriteString(``)
b.WriteString(`| Parca | Metraj | MT Giris Fiyat | Pr.Br. | USD Tutar | TRY Tutar | `)
b.WriteString(`
`)
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(``)
b.WriteString(fmt.Sprintf(`| %s | `, htmlEsc(p)))
b.WriteString(fmt.Sprintf(`%s | `, htmlEsc(formatMeterLabel(row))))
b.WriteString(fmt.Sprintf(`%s | `, htmlEsc(unitLabel)))
b.WriteString(fmt.Sprintf(`%s | `, htmlEsc(curLabel)))
b.WriteString(fmt.Sprintf(`%s | `, formatMoney2(row.USD)))
b.WriteString(fmt.Sprintf(`%s | `, formatMoney2(row.TRY)))
b.WriteString(`
`)
}
totalMeterLabel := "-"
if totalMeter > 0 {
totalMeterLabel = fmt.Sprintf("%s MT", formatQty2(totalMeter))
}
b.WriteString(``)
b.WriteString(`| TOPLAM | `)
b.WriteString(fmt.Sprintf(`%s | `, htmlEsc(totalMeterLabel)))
b.WriteString(` | | `)
b.WriteString(fmt.Sprintf(`%s | `, formatMoney2(totalUSD)))
b.WriteString(fmt.Sprintf(`%s | `, formatMoney2(totalTRY)))
b.WriteString(`
`)
b.WriteString(`
`)
}
renderLaborTable("Iscilik Fiyatlari (CM2)", labor)
renderMaterialTable("Malzeme Fiyatlari (DT/TP, maliyete dahil)", material)
renderFabricTable("Kumas Fiyatlari (FABRIC, maliyete dahil)", fabric)
b.WriteString(`
Bu mail BaggiSS App uzerinden otomatik gonderilmistir.
`)
b.WriteString(`
`)
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
}