package routes import ( "bytes" "database/sql" "errors" "fmt" "github.com/gorilla/mux" "github.com/jung-kurt/gofpdf" "log" "math" "net/http" "runtime/debug" "sort" "strings" "time" ) /* =========================================================== 1) SABİTLER / RENKLER / TİPLER =========================================================== */ // Baggi renkleri var ( baggiGoldR, baggiGoldG, baggiGoldB = 201, 162, 39 baggiCreamR, baggiCreamG, baggiCreamB = 255, 254, 249 baggiGrayBorderR, baggiGrayBorderG, baggiGrayBorderB = 187, 187, 187 ) // Beden kategorileri (frontend birebir) const ( catAyk = "ayk" catYas = "yas" catPan = "pan" catGom = "gom" catTak = "tak" catAksbir = "aksbir" ) var categoryOrder = []string{catAyk, catYas, catPan, catGom, catTak, catAksbir} var categoryTitle = map[string]string{ catAyk: " AYAKKABI", catYas: " YAŞ", catPan: " PANTOLON", catGom: " GÖMLEK", catTak: " TAKIM ELBİSE", catAksbir: " AKSESUAR", } /* =========================================================== HEADER MODEL =========================================================== */ type OrderHeader struct { OrderHeaderID string OrderNumber string CurrAccCode string CurrAccName string DocCurrency string OrderDate time.Time Description string InternalDesc string OfficeCode string CreatedUser string CustomerRep string // 🆕 Müşteri Temsilcisi } /* =========================================================== RAW LINE MODEL =========================================================== */ type OrderLineRaw struct { OrderLineID sql.NullString ItemCode string ColorCode string ItemDim1Code sql.NullString ItemDim2Code sql.NullString Qty1 sql.NullFloat64 Price sql.NullFloat64 DocCurrencyCode sql.NullString DeliveryDate sql.NullTime LineDescription sql.NullString UrunAnaGrubu sql.NullString UrunAltGrubu sql.NullString IsClosed sql.NullBool WithHoldingTaxType sql.NullString DOVCode sql.NullString PlannedDateOfLading sql.NullTime CostCenterCode sql.NullString VatCode sql.NullString VatRate sql.NullFloat64 } /* =========================================================== PDF SATIR MODELİ =========================================================== */ type PdfRow struct { Model string Color string GroupMain string GroupSub string Description string Category string SizeQty map[string]int TotalQty int Price float64 Currency string Amount float64 Termin string IsClosed bool OrderLineIDs map[string]string ClosedSizes map[string]bool // 🆕 her beden için IsClosed bilgisi } /* =========================================================== PDF LAYOUT STRUCT =========================================================== */ type pdfLayout struct { PageW, PageH float64 MarginL float64 MarginR float64 MarginT float64 MarginB float64 ColModelW float64 ColRenkW float64 ColGroupW float64 ColGroupW2 float64 ColDescLeft float64 ColDescRight float64 ColDescW float64 ColQtyW float64 ColPriceW float64 ColCurW float64 ColAmountW float64 ColTerminW float64 CentralW float64 HeaderMainH float64 HeaderSizeH float64 RowH float64 } /* genel cell padding */ const OcellPadX = 2 /* =========================================================== PDF LAYOUT OLUŞTURUCU =========================================================== */ /* =========================================================== PDF LAYOUT OLUŞTURUCU — AÇIKLAMA TEK KOLON =========================================================== */ func newPdfLayout(pdf *gofpdf.Fpdf) pdfLayout { pageW, pageH := pdf.GetPageSize() l := pdfLayout{ PageW: pageW, PageH: pageH, MarginL: 10, MarginR: 10, MarginT: 10, MarginB: 12, RowH: 7, HeaderMainH: 8, HeaderSizeH: 6, } totalW := pageW - l.MarginL - l.MarginR /* -------------------------------------------------------- SOL BLOK GÜNCEL – MODEL/RENK ÇAKIŞMASI GİDERİLDİ -------------------------------------------------------- */ l.ColModelW = 24 // eski 18 → genişletildi l.ColRenkW = 24 // eski 14 → biraz geniş l.ColGroupW = 20 l.ColGroupW2 = 20 /* -------------------------------------------------------- AÇIKLAMA = TEK GENİŞ KOLON (kategori listesi + açıklama içerir) -------------------------------------------------------- */ l.ColDescLeft = 50 // açıklama başlığı + kategori alanı (artık tek kolon) l.ColDescRight = 0 // kullanılmıyor (0 bırakılmalı!) left := l.ColModelW + l.ColRenkW + l.ColGroupW + l.ColGroupW2 + l.ColDescLeft /* -------------------------------------------------------- SAĞ BLOK -------------------------------------------------------- */ l.ColQtyW = 12 l.ColPriceW = 16 l.ColCurW = 10 l.ColAmountW = 20 l.ColTerminW = 20 right := l.ColQtyW + l.ColPriceW + l.ColCurW + l.ColAmountW + l.ColTerminW /* -------------------------------------------------------- ORTA BLOK (BEDEN 16 KOLON) -------------------------------------------------------- */ l.CentralW = totalW - left - right if l.CentralW < 70 { l.CentralW = 70 } return l } /* =========================================================== HELPER FONKSİYONLAR =========================================================== */ func safeTrimUpper(s string) string { return strings.ToUpper(strings.TrimSpace(s)) } func f64(v sql.NullFloat64) float64 { if !v.Valid { return 0 } return v.Float64 } func s64(v sql.NullString) string { if !v.Valid { return "" } return v.String } func sOrEmpty(v sql.NullString) string { if !v.Valid { return "" } return strings.TrimSpace(v.String) } func normalizeBedenLabelGo(v string) string { // 1️⃣ NULL / boş / whitespace → " " (aksbir null kolonu) s := strings.TrimSpace(v) if s == "" { return " " // 🔥 NULL BEDEN → boş kolon } // 2️⃣ Uppercase s = strings.ToUpper(s) /* -------------------------------------------------- 🔥 AKSBİR ÖZEL (STD eş anlamlıları) -------------------------------------------------- */ switch s { case "STD", "STANDART", "STANDARD", "ONE SIZE", "ONESIZE": return "STD" } /* -------------------------------------------------- 🔢 SADECE "CM" VARSA → NUMERİK KISMI AL 120CM / 120 CM → 120 ❌ 105 / 110 / 120 → DOKUNMA -------------------------------------------------- */ if strings.HasSuffix(s, "CM") { num := strings.TrimSpace(strings.TrimSuffix(s, "CM")) if num != "" { return num } } /* -------------------------------------------------- HARF BEDENLER (DOKUNMA) -------------------------------------------------- */ switch s { case "XS", "S", "M", "L", "XL", "2XL", "3XL", "4XL", "5XL", "6XL", "7XL": return s } // 4️⃣ Sayısal veya başka değerler → olduğu gibi return s } func detectBedenGroupGo(bedenList []string, ana, alt string) string { ana = safeTrimUpper(ana) alt = safeTrimUpper(alt) for _, b := range bedenList { switch b { case "XS", "S", "M", "L", "XL": return catGom } } if strings.Contains(ana, "PANTOLON") { return catPan } if strings.Contains(alt, "ÇOCUK") || strings.Contains(alt, "GARSON") { return catYas } return catTak } func defaultSizeListFor(cat string) []string { switch cat { case catAyk: return []string{"39", "40", "41", "42", "43", "44", "45"} case catYas: return []string{"2", "4", "6", "8", "10", "12", "14"} case catPan: return []string{"38", "40", "42", "44", "46", "48", "50", "52", "54", "56", "58", "60", "62", "64", "66", "68"} case catGom: return []string{"XS", "S", "M", "L", "XL", "2XL", "3XL", "4XL", "5XL", "6XL", "7XL"} case catTak: return []string{"44", "46", "48", "50", "52", "54", "56", "58", "60", "62", "64", "66", "68", "70", "72", "74"} case catAksbir: return []string{"", "44", "STD", "110", "115", "120", "125", "130", "135"} } return []string{} } func contains(list []string, v string) bool { for _, x := range list { if x == v { return true } } return false } /* =========================================================== 2) PDF OLUŞTURUCU (A4 YATAY + FOOTER) =========================================================== */ func newOrderPdf() (*gofpdf.Fpdf, error) { pdf := gofpdf.New("L", "mm", "A4", "") pdf.SetMargins(10, 10, 10) pdf.SetAutoPageBreak(false, 12) if err := registerDejavuFonts(pdf, "dejavu", "dejavu-b"); err != nil { return nil, err } // Footer: sayfa numarası pdf.AliasNbPages("") pdf.SetFooterFunc(func() { pdf.SetY(-10) pdf.SetFont("dejavu", "", 8) txt := fmt.Sprintf("Sayfa %d/{nb}", pdf.PageNo()) pdf.CellFormat(0, 10, txt, "", 0, "R", false, 0, "") }) return pdf, nil } /* =========================================================== 3) DB FONKSİYONLARI (HEADER + LINES) =========================================================== */ // HEADER func getOrderHeaderFromDB(db *sql.DB, orderID string) (*OrderHeader, error) { row := db.QueryRow(` SELECT CAST(h.OrderHeaderID AS varchar(36)), h.OrderNumber, h.CurrAccCode, d.CurrAccDescription, h.DocCurrencyCode, h.OrderDate, h.Description, h.InternalDescription, h.OfficeCode, h.CreatedUserName, ISNULL(( SELECT TOP (1) ca.AttributeDescription FROM BAGGI_V3.dbo.cdCurrAccAttributeDesc AS ca WITH (NOLOCK) WHERE ca.CurrAccTypeCode = 3 AND ca.AttributeTypeCode = 2 -- 🟡 Müşteri Temsilcisi AND ca.AttributeCode = f.CustomerAtt02 AND ca.LangCode = 'TR' ), '') AS CustomerRep FROM BAGGI_V3.dbo.trOrderHeader AS h LEFT JOIN BAGGI_V3.dbo.cdCurrAccDesc AS d ON h.CurrAccCode = d.CurrAccCode LEFT JOIN BAGGI_V3.dbo.CustomerAttributesFilter AS f ON h.CurrAccCode = f.CurrAccCode WHERE h.OrderHeaderID = @p1 `, orderID) var h OrderHeader var orderDate sql.NullTime var orderNumber, currAccCode, currAccName, docCurrency sql.NullString var description, internalDesc, officeCode, createdUser, customerRep sql.NullString err := row.Scan( &h.OrderHeaderID, &orderNumber, &currAccCode, &currAccName, &docCurrency, &orderDate, &description, &internalDesc, &officeCode, &createdUser, &customerRep, // 🆕 buradan geliyor ) if err != nil { return nil, err } h.OrderNumber = sOrEmpty(orderNumber) h.CurrAccCode = sOrEmpty(currAccCode) h.CurrAccName = sOrEmpty(currAccName) h.DocCurrency = sOrEmpty(docCurrency) h.Description = sOrEmpty(description) h.InternalDesc = sOrEmpty(internalDesc) h.OfficeCode = sOrEmpty(officeCode) h.CreatedUser = sOrEmpty(createdUser) h.CustomerRep = sOrEmpty(customerRep) if orderDate.Valid { h.OrderDate = orderDate.Time } return &h, nil } // LINES func getOrderLinesFromDB(db *sql.DB, orderID string) ([]OrderLineRaw, error) { rows, err := db.Query(` SELECT CAST(L.OrderLineID AS varchar(36)), L.ItemCode, L.ColorCode, L.ItemDim1Code, L.ItemDim2Code, L.Qty1, L.Price, L.DocCurrencyCode, L.DeliveryDate, L.LineDescription, P.ProductAtt01Desc, P.ProductAtt02Desc, L.IsClosed, L.WithHoldingTaxTypeCode, L.DOVCode, L.PlannedDateOfLading, L.CostCenterCode, L.VatCode, L.VatRate FROM BAGGI_V3.dbo.trOrderLine AS L LEFT JOIN ProductFilterWithDescription('TR') AS P ON LTRIM(RTRIM(P.ProductCode)) = LTRIM(RTRIM(L.ItemCode)) WHERE L.OrderHeaderID = @p1 ORDER BY L.SortOrder, L.OrderLineID `, orderID) if err != nil { return nil, err } defer rows.Close() var out []OrderLineRaw for rows.Next() { var l OrderLineRaw if err := rows.Scan( &l.OrderLineID, &l.ItemCode, &l.ColorCode, &l.ItemDim1Code, &l.ItemDim2Code, &l.Qty1, &l.Price, &l.DocCurrencyCode, &l.DeliveryDate, &l.LineDescription, &l.UrunAnaGrubu, &l.UrunAltGrubu, &l.IsClosed, &l.WithHoldingTaxType, &l.DOVCode, &l.PlannedDateOfLading, &l.CostCenterCode, &l.VatCode, &l.VatRate, ); err != nil { return nil, err } out = append(out, l) } return out, rows.Err() } /* =========================================================== 4) NORMALIZE + CATEGORY MAP =========================================================== */ func normalizeOrderLinesForPdf(lines []OrderLineRaw) []PdfRow { type comboKey struct { Model, Color, Color2 string } merged := make(map[comboKey]*PdfRow) for _, raw := range lines { // ❌ ARTIK KAPALI SATIRLARI ATMAYACAĞIZ // if raw.IsClosed.Valid && raw.IsClosed.Bool { // continue // } model := safeTrimUpper(raw.ItemCode) color := safeTrimUpper(raw.ColorCode) color2 := safeTrimUpper(s64(raw.ItemDim2Code)) displayColor := color if color2 != "" { displayColor = fmt.Sprintf("%s-%s", color, color2) } key := comboKey{model, color, color2} if _, ok := merged[key]; !ok { merged[key] = &PdfRow{ Model: model, Color: displayColor, GroupMain: s64(raw.UrunAnaGrubu), GroupSub: s64(raw.UrunAltGrubu), Description: s64(raw.LineDescription), SizeQty: make(map[string]int), Currency: s64(raw.DocCurrencyCode), Price: f64(raw.Price), OrderLineIDs: make(map[string]string), ClosedSizes: make(map[string]bool), // 🆕 } } row := merged[key] // beden rawBeden := s64(raw.ItemDim1Code) if raw.ItemDim1Code.Valid { rawBeden = raw.ItemDim1Code.String } normalized := normalizeBedenLabelGo(rawBeden) qty := int(math.Round(f64(raw.Qty1))) if qty > 0 { row.SizeQty[normalized] += qty row.TotalQty += qty // 🆕 Bu beden kapalı satırdan geldiyse işaretle if raw.IsClosed.Valid && raw.IsClosed.Bool { row.ClosedSizes[normalized] = true } } // OrderLineID eşleştirmesi if raw.OrderLineID.Valid { row.OrderLineIDs[normalized] = raw.OrderLineID.String } // Termin if row.Termin == "" && raw.DeliveryDate.Valid { row.Termin = raw.DeliveryDate.Time.Format("02.01.2006") } } // finalize out := make([]PdfRow, 0, len(merged)) for _, r := range merged { var sizes []string for s := range r.SizeQty { sizes = append(sizes, s) } r.Category = detectBedenGroupGo(sizes, r.GroupMain, r.GroupSub) r.Amount = float64(r.TotalQty) * r.Price out = append(out, *r) } // Sıralama: Model → Renk → Category sort.Slice(out, func(i, j int) bool { if out[i].Model != out[j].Model { return out[i].Model < out[j].Model } if out[i].Color != out[j].Color { return out[i].Color < out[j].Color } return out[i].Category < out[j].Category }) return out } /* =========================================================== 5) CATEGORY → SIZE MAP (HEADER İÇİN) =========================================================== */ type CategorySizeMap map[string][]string // kategori beden map (global – TÜM GRİD İÇİN TEK HEADER) func buildCategorySizeMap(rows []PdfRow) CategorySizeMap { cm := make(CategorySizeMap) // Her kategori için sabit default listeleri kullan for _, cat := range categoryOrder { base := defaultSizeListFor(cat) if len(base) > 0 { cm[cat] = append([]string{}, base...) } } // İstersen ekstra bedenler varsa ekle (opsiyonel) for _, r := range rows { c := r.Category if c == "" { c = catTak } if _, ok := cm[c]; !ok { cm[c] = []string{} } for size := range r.SizeQty { if !contains(cm[c], size) { cm[c] = append(cm[c], size) } } } return cm } /* =========================================================== ORDER HEADER (Logo + Gold Label + Sağ Bilgi Kutusu) =========================================================== */ func drawOrderHeader(pdf *gofpdf.Fpdf, h *OrderHeader, showDesc bool) float64 { pageW, _ := pdf.GetPageSize() marginL := 10.0 y := 8.0 /* ---------------------------------------------------- 1) LOGO ---------------------------------------------------- */ if logoPath, err := resolvePdfImagePath("Baggi-Tekstil-A.s-Logolu.jpeg"); err == nil { pdf.ImageOptions(logoPath, marginL, y, 32, 0, false, gofpdf.ImageOptions{}, 0, "") } /* ---------------------------------------------------- 2) ALTIN BAŞLIK BAR ---------------------------------------------------- */ titleW := 150.0 titleX := marginL + 40 titleY := y + 2 pdf.SetFillColor(149, 113, 22) // Baggi altın pdf.Rect(titleX, titleY, titleW, 10, "F") pdf.SetFont("dejavu-b", "", 13) pdf.SetTextColor(255, 255, 255) pdf.SetXY(titleX+4, titleY+2) pdf.CellFormat(titleW-8, 6, "BAGGI TEKSTİL - SİPARİŞ FORMU", "", 0, "L", false, 0, "") /* ---------------------------------------------------- 3) SAĞ TARAF BİLGİ KUTUSU ---------------------------------------------------- */ boxW := 78.0 boxH := 30.0 boxX := pageW - marginL - boxW boxY := y - 2 pdf.SetDrawColor(180, 180, 180) pdf.Rect(boxX, boxY, boxW, boxH, "") pdf.SetFont("dejavu-b", "", 9) pdf.SetTextColor(149, 113, 22) rep := strings.TrimSpace(h.CustomerRep) if rep == "" { rep = strings.TrimSpace(h.CreatedUser) } info := []string{ "Formun Basılma Tarihi: " + time.Now().Format("02.01.2006"), "Sipariş Tarihi: " + h.OrderDate.Format("02.01.2006"), "Sipariş No: " + h.OrderNumber, "Müşteri Temsilcisi: " + rep, // 🔥 YENİ EKLENDİ "Cari Kod: " + h.CurrAccCode, "Müşteri: " + h.CurrAccName, } iy := boxY + 3 for _, line := range info { pdf.SetXY(boxX+3, iy) pdf.CellFormat(boxW-6, 4.5, line, "", 0, "L", false, 0, "") iy += 4.5 } /* ---------------------------------------------------- 4) ALT AYIRICI ÇİZGİ ---------------------------------------------------- */ lineY := boxY + boxH + 3 pdf.SetDrawColor(120, 120, 120) pdf.Line(marginL, lineY, pageW-marginL, lineY) pdf.SetDrawColor(200, 200, 200) y = lineY + 4 /* ---------------------------------------------------- 5) AÇIKLAMA (Varsa) ---------------------------------------------------- */ if showDesc && strings.TrimSpace(h.Description) != "" { text := strings.TrimSpace(h.Description) pdf.SetFont("dejavu", "", 8) // wrap’te kullanılacak font lineH := 4.0 textW := pageW - marginL*2 - 52 // Kaç satır olacağını hesapla lines := pdf.SplitLines([]byte(text), textW) descBoxH := float64(len(lines))*lineH + 4 // min boşluk if descBoxH < 10 { descBoxH = 10 } pdf.SetDrawColor(210, 210, 210) pdf.Rect(marginL, y, pageW-marginL*2, descBoxH, "") // Başlık pdf.SetFont("dejavu-b", "", 8) pdf.SetTextColor(149, 113, 22) pdf.SetXY(marginL+3, y+2) pdf.CellFormat(40, 4, "Sipariş Genel Açıklaması:", "", 0, "L", false, 0, "") // Metin pdf.SetFont("dejavu", "", 8) pdf.SetTextColor(30, 30, 30) pdf.SetXY(marginL+48, y+2) pdf.MultiCell(textW, lineH, text, "", "L", false) y += descBoxH + 3 } return y } /* =========================================================== GRID HEADER — 2 katmanlı + 16 beden kolonlu =========================================================== */ /* =========================================================== GRID HEADER — AÇIKLAMA içinde kategori listesi + 16 beden kolonu =========================================================== */ func drawGridHeader(pdf *gofpdf.Fpdf, layout pdfLayout, startY float64, catSizes CategorySizeMap) float64 { pdf.SetFont("dejavu-b", "", 6) pdf.SetDrawColor(baggiGrayBorderR, baggiGrayBorderG, baggiGrayBorderB) pdf.SetFillColor(baggiCreamR, baggiCreamG, baggiCreamB) pdf.SetTextColor(20, 20, 20) // 🟣 TÜM HEADER YAZILARI SİYAH y := startY x := layout.MarginL totalHeaderH := float64(len(categoryOrder)) * layout.HeaderSizeH centerLabel := func(h float64) float64 { return y + (h/2.0 - 3) } /* ---------------------------------------------------- SOL BLOK (Model – Renk – Grup1 – Grup2) ---------------------------------------------------- */ cols := []struct { w float64 t string }{ {layout.ColModelW, "MODEL"}, {layout.ColRenkW, "RENK"}, {layout.ColGroupW, "ÜRÜN ANA"}, {layout.ColGroupW2, "ÜRÜN ALT"}, } for _, c := range cols { pdf.Rect(x, y, c.w, totalHeaderH, "DF") pdf.SetXY(x, centerLabel(totalHeaderH)) pdf.CellFormat(c.w, 6, c.t, "", 0, "C", false, 0, "") x += c.w } /* ---------------------------------------------------- AÇIKLAMA BAŞLIĞI — TEK HÜCRE & DİKEY ORTALAMA ---------------------------------------------------- */ descX := x descW := layout.ColDescLeft pdf.Rect(descX, y, descW, totalHeaderH, "DF") // AÇIKLAMA yazısı ortalanacak pdf.SetXY(descX, y+(totalHeaderH/2-3)) pdf.CellFormat(descW, 6, "AÇIKLAMA", "", 0, "C", false, 0, "") /* ---------------------------------------------------- AÇIKLAMA sağında kategori listesi dikey şekilde ---------------------------------------------------- */ catX := descX + 1 catY := y + 2 pdf.SetFont("dejavu", "", 6.2) for _, cat := range categoryOrder { label := categoryTitle[cat] pdf.SetXY(catX+2, catY) pdf.CellFormat(descW-4, 4.8, label, "", 0, "L", false, 0, "") catY += layout.HeaderSizeH } /* ---------------------------------------------------- 16’lı BEDEN BLOĞU ---------------------------------------------------- */ x = descX + descW colW := layout.CentralW / 16.0 cy := y for _, cat := range categoryOrder { // Arka plan pdf.SetFillColor(baggiCreamR, baggiCreamG, baggiCreamB) pdf.Rect(x, cy, layout.CentralW, layout.HeaderSizeH, "DF") sizes := catSizes[cat] if len(sizes) == 0 { sizes = defaultSizeListFor(cat) } if cat == catAksbir { pdf.SetFont("dejavu", "", 5) // sadece aksesuar için küçük font } else { pdf.SetFont("dejavu", "", 6) // diğer tüm kategoriler normal font } xx := x for i := 0; i < 16; i++ { pdf.Rect(xx, cy, colW, layout.HeaderSizeH, "") if i < len(sizes) { pdf.SetXY(xx, cy+1) pdf.CellFormat(colW, layout.HeaderSizeH-2, sizes[i], "", 0, "C", false, 0, "") } xx += colW } cy += layout.HeaderSizeH } /* ---------------------------------------------------- SAĞ BLOK (ADET – FİYAT – PB – TUTAR – TERMİN) ---------------------------------------------------- */ rightX := x + 16*colW rightCols := []struct { w float64 t string }{ {layout.ColQtyW, "ADET"}, {layout.ColPriceW, "FİYAT"}, {layout.ColCurW, "PB"}, {layout.ColAmountW, "TUTAR"}, {layout.ColTerminW, "TERMİN"}, } for _, c := range rightCols { pdf.Rect(rightX, y, c.w, totalHeaderH, "DF") pdf.SetXY(rightX, centerLabel(totalHeaderH)) pdf.CellFormat(c.w, 6, c.t, "", 0, "C", false, 0, "") rightX += c.w } return y + totalHeaderH } /* =========================================================== AÇIKLAMA WRAP + DİNAMİK SATIR YÜKSEKLİĞİ =========================================================== */ // Açıklama kolonunu çok satırlı yazan helper func drawWrappedCell(pdf *gofpdf.Fpdf, text string, x, y, w, h float64) { txt := strings.TrimSpace(text) if txt == "" { return } lineH := 3.2 lines := pdf.SplitLines([]byte(txt), w-2*OcellPadX) cy := y + 1 for _, ln := range lines { if cy+lineH > y+h { break } pdf.SetXY(x+OcellPadX, cy) pdf.CellFormat(w-2*OcellPadX, lineH, string(ln), "", 0, "L", false, 0, "") cy += lineH } } // Açıklamaya göre satır yüksekliği hesaplar func calcRowHeight(pdf *gofpdf.Fpdf, layout pdfLayout, row PdfRow) float64 { base := layout.RowH desc := strings.TrimSpace(row.Description) if desc == "" { return base } // Açıklama tek kolonda (ColDescLeft) render ediliyor. // ColDescW set edilmediği için 0 kalabiliyor; bu durumda SplitLines patlayabiliyor. descW := layout.ColDescLeft if descW <= float64(2*OcellPadX) { descW = layout.ColDescLeft + layout.ColDescRight } if descW <= float64(2*OcellPadX) { return base } lines := pdf.SplitLines([]byte(desc), descW-float64(2*OcellPadX)) lineH := 3.2 h := float64(len(lines))*lineH + 2 if h < base { h = base } return h } /* =========================================================== SATIR ÇİZİMİ — 16 kolonlu beden dizilimi (sola yaslı) =========================================================== */ /* =========================================================== SATIR ÇİZİMİ — Tek açıklama sütunu + 16 beden kolonu =========================================================== */ func drawPdfRow(pdf *gofpdf.Fpdf, layout pdfLayout, y float64, row PdfRow, catSizes CategorySizeMap, rowH float64) float64 { pdf.SetFont("dejavu", "", 7) pdf.SetDrawColor(200, 200, 200) pdf.SetLineWidth(0.15) pdf.SetTextColor(0, 0, 0) x := layout.MarginL h := rowH centerY := func() float64 { return y + (h-3.5)/2 } /* ---------------------------------------------------- 1) SOL BLOK MODEL – RENK – ÜRÜN ANA – ÜRÜN ALT ---------------------------------------------------- */ cols := []struct { w float64 v string }{ {layout.ColModelW, row.Model}, {layout.ColRenkW, row.Color}, {layout.ColGroupW, row.GroupMain}, {layout.ColGroupW2, row.GroupSub}, } for _, c := range cols { pdf.Rect(x, y, c.w, h, "") pdf.SetXY(x+1.3, centerY()) pdf.CellFormat(c.w-2.6, 3.5, c.v, "", 0, "L", false, 0, "") x += c.w } /* ---------------------------------------------------- 2) AÇIKLAMA (TEK BÜYÜK KOLON) ---------------------------------------------------- */ descW := layout.ColDescLeft // açıklama = sadece sol kolon pdf.Rect(x, y, descW, h, "") drawWrappedCell(pdf, row.Description, x, y, descW, h) x += descW /* ---------------------------------------------------- 3) 16 BEDEN KOLONU ---------------------------------------------------- */ colW := layout.CentralW / 16.0 // kategorinin beden listesi sizes := catSizes[row.Category] if len(sizes) == 0 { tmp := make([]string, 0, len(row.SizeQty)) for s := range row.SizeQty { tmp = append(tmp, s) } sort.Strings(tmp) sizes = tmp } xx := x for i := 0; i < 16; i++ { if i < len(sizes) { lbl := sizes[i] if q, ok := row.SizeQty[lbl]; ok && q > 0 { // 🔍 Bu beden kapalı mı? isClosedSize := row.ClosedSizes != nil && row.ClosedSizes[lbl] if isClosedSize { // Gri dolgu + beyaz yazı pdf.SetFillColor(220, 220, 220) // açık gri zemin pdf.Rect(xx, y, colW, h, "DF") // doldurulmuş hücre pdf.SetTextColor(255, 255, 255) // beyaz text } else { // Normal hücre: sadece border pdf.Rect(xx, y, colW, h, "") pdf.SetTextColor(0, 0, 0) // siyah text } pdf.SetXY(xx, centerY()) pdf.CellFormat(colW, 3.5, fmt.Sprintf("%d", q), "", 0, "C", false, 0, "") // Sonraki işler için text rengini resetle if isClosedSize { pdf.SetTextColor(0, 0, 0) } } else { // Bu beden kolonunda quantity yoksa normal boş hücre pdf.Rect(xx, y, colW, h, "") } } else { // 16 kolonun kalanları (header'da var ama bu kategoride kullanılmayan bedenler) pdf.Rect(xx, y, colW, h, "") } xx += colW } x = xx /* ---------------------------------------------------- 4) SAĞ BLOK: ADET – FİYAT – PB – TUTAR – TERMİN ---------------------------------------------------- */ rightCols := []struct { w float64 v string alg string }{ {layout.ColQtyW, fmt.Sprintf("%d", row.TotalQty), "C"}, {layout.ColPriceW, fmt.Sprintf("%.2f", row.Price), "R"}, {layout.ColCurW, row.Currency, "C"}, {layout.ColAmountW, fmt.Sprintf("%.2f", row.Amount), "R"}, {layout.ColTerminW, row.Termin, "C"}, } for _, c := range rightCols { pdf.Rect(x, y, c.w, h, "") pdf.SetXY(x+0.6, centerY()) pdf.CellFormat(c.w-1.2, 3.5, c.v, "", 0, c.alg, false, 0, "") x += c.w } return y + h } /* =========================================================== FORMATLAYICI: TR PARA BİÇİMİ (1.234.567,89) =========================================================== */ /* =========================================================== TOPLAM TUTAR / KDV / KDV DAHİL TOPLAM KUTUSU (Sipariş Genel Açıklaması ile grid header arasında) =========================================================== */ /* =========================================================== PREMIUM BAGGI GOLD TOTALS BOX (Sipariş Açıklaması ile Grid Header arasındaki kutu) =========================================================== */ func drawTotalsBox( pdf *gofpdf.Fpdf, layout pdfLayout, startY float64, subtotal float64, hasVat bool, vatRate float64, vatAmount float64, totalWithVat float64, currency string, ) float64 { x := layout.MarginL w := layout.PageW - layout.MarginL - layout.MarginR lineH := 6.0 rows := 1 if hasVat { rows = 3 } boxH := float64(rows)*lineH + 5 /* ---------------------------------------------------- ARKA PLAN + ALTIN ÇERÇEVE ---------------------------------------------------- */ pdf.SetFillColor(255, 253, 245) // yumuşak krem pdf.SetDrawColor(149, 113, 22) // Baggi gold pdf.SetLineWidth(0.4) pdf.Rect(x, startY, w, boxH, "DF") /* ---------------------------------------------------- Genel metin stilleri ---------------------------------------------------- */ labelX := x + 4 valueX := x + w - 70 // değerlerin sağda hizalanacağı kolon pdf.SetTextColor(149, 113, 22) // Sol başlık gold pdf.SetFont("dejavu-b", "", 8.5) y := startY + 2 /* ---------------------------------------------------- 1️⃣ TOPLAM TUTAR ---------------------------------------------------- */ pdf.SetXY(labelX, y) pdf.CellFormat(80, lineH, "TOPLAM TUTAR", "", 0, "L", false, 0, "") pdf.SetTextColor(201, 162, 39) pdf.SetFont("dejavu-b", "", 9) pdf.SetXY(valueX, y) pdf.CellFormat(65, lineH, fmt.Sprintf("%s %s", formatCurrencyTR(subtotal), currency), "", 0, "R", false, 0, "") y += lineH /* ---------------------------------------------------- 2️⃣ KDV (opsiyonel) ---------------------------------------------------- */ if hasVat { pdf.SetTextColor(149, 113, 22) // gold başlık pdf.SetFont("dejavu-b", "", 8.5) pdf.SetXY(labelX, y) pdf.CellFormat(80, lineH, fmt.Sprintf("KDV (%%%g)", vatRate), "", 0, "L", false, 0, "") pdf.SetTextColor(20, 20, 20) pdf.SetFont("dejavu-b", "", 9) pdf.SetXY(valueX, y) pdf.CellFormat(65, lineH, fmt.Sprintf("%s %s", formatCurrencyTR(vatAmount), currency), "", 0, "R", false, 0, "") y += lineH /* ---------------------------------------------------- 3️⃣ KDV DAHİL TOPLAM ---------------------------------------------------- */ pdf.SetTextColor(201, 162, 39) pdf.SetFont("dejavu-b", "", 8.5) pdf.SetXY(labelX, y) pdf.CellFormat(80, lineH, "KDV DAHİL TOPLAM TUTAR", "", 0, "L", false, 0, "") pdf.SetTextColor(20, 20, 20) pdf.SetFont("dejavu-b", "", 9) pdf.SetXY(valueX, y) pdf.CellFormat(65, lineH, fmt.Sprintf("%s %s", formatCurrencyTR(totalWithVat), currency), "", 0, "R", false, 0, "") } /* ---------------------------------------------------- Kutu altı boşluk ---------------------------------------------------- */ return startY + boxH + 4 } /* =========================================================== GRUP TOPLAM BAR (Frontend tarzı sarı bar) =========================================================== */ func drawGroupSummaryBar(pdf *gofpdf.Fpdf, layout pdfLayout, groupName string, totalQty int, totalAmount float64, currency string) float64 { y := pdf.GetY() x := layout.MarginL w := layout.PageW - layout.MarginL - layout.MarginR h := 7.0 // Açık sarı zemin pdf.SetFillColor(255, 249, 205) // #fff9cd benzeri pdf.SetDrawColor(214, 192, 106) pdf.Rect(x, y, w, h, "DF") pdf.SetFont("dejavu-b", "", 8.5) pdf.SetTextColor(20, 20, 20) leftTxt := strings.ToUpper(strings.TrimSpace(groupName)) if leftTxt == "" { leftTxt = "GENEL" } pdf.SetXY(x+2, y+1.2) pdf.CellFormat(w*0.40, h-2.4, leftTxt, "", 0, "L", false, 0, "") rightTxt := fmt.Sprintf( "TOPLAM %s ADET: %d TOPLAM %s TUTAR: %s %s", leftTxt, totalQty, leftTxt, formatCurrencyTR(totalAmount), currency, ) pdf.SetXY(x+w*0.35, y+1.2) pdf.CellFormat(w*0.60-2, h-2.4, rightTxt, "", 0, "R", false, 0, "") // reset pdf.SetTextColor(0, 0, 0) pdf.SetDrawColor(201, 162, 39) pdf.SetY(y + h) return y + h } /* =========================================================== MULTIPAGE RENDER ENGINE (Header + GridHeader + Rows) =========================================================== */ func renderOrderGrid(pdf *gofpdf.Fpdf, header *OrderHeader, rows []PdfRow, hasVat bool, vatRate float64) { layout := newPdfLayout(pdf) catSizes := buildCategorySizeMap(rows) // Grup: ÜRÜN ANA GRUBU type group struct { Name string Rows []PdfRow Adet int Tutar float64 } groups := map[string]*group{} var order []string for _, r := range rows { name := strings.TrimSpace(r.GroupMain) if name == "" { name = "GENEL" } g, ok := groups[name] if !ok { g = &group{Name: name} groups[name] = g order = append(order, name) } g.Rows = append(g.Rows, r) g.Adet += r.TotalQty g.Tutar += r.Amount } groupSummaryH := 7.0 // 🔹 Genel toplam (grid içindeki satırlardan) var subtotal float64 for _, r := range rows { subtotal += r.Amount } // 🔹 KDV hesapla (hasVat ve vatRate, OrderPDFHandler'dan geliyor) var vatAmount float64 totalWithVat := subtotal if hasVat && vatRate > 0 { vatAmount = subtotal * vatRate / 100.0 totalWithVat = subtotal + vatAmount } var y float64 firstPage := true newPage := func(showDesc bool, showTotals bool) { pdf.AddPage() // Üst header (logo + sağ kutu + genel açıklama) y = drawOrderHeader(pdf, header, showDesc) // 🔸 İlk sayfada, header ile grid arasında TOPLAM kutusu if showTotals { y = drawTotalsBox( pdf, layout, y, subtotal, hasVat, vatRate, vatAmount, totalWithVat, header.DocCurrency, ) } // Grid header y = drawGridHeader(pdf, layout, y, catSizes) y += 1 } // İlk sayfa: açıklama + toplam kutusu newPage(firstPage, true) firstPage = false for _, name := range order { g := groups[name] for _, row := range g.Rows { rh := calcRowHeight(pdf, layout, row) if y+rh+groupSummaryH+2 > layout.PageH-layout.MarginB { // Sonraki sayfalarda: açıklama yok, toplam kutusu yok newPage(false, false) } y = drawPdfRow(pdf, layout, y, row, catSizes, rh) pdf.SetY(y) // 🔹 satırın altına imleci getir } // Grup toplam barı için yer kontrolü if y+groupSummaryH+2 > layout.PageH-layout.MarginB { newPage(false, false) } y = drawGroupSummaryBar(pdf, layout, g.Name, g.Adet, g.Tutar, header.DocCurrency) y += 1 } // ⚠️ Eski alttaki "Genel Toplam" yazdırma kaldırıldı; toplam kutusu artık üstte. } /* =========================================================== HTTP HANDLER → /api/order/pdf/{id} =========================================================== */ func OrderPDFHandler(db *sql.DB) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { orderID := mux.Vars(r)["id"] defer func() { if rec := recover(); rec != nil { log.Printf("❌ PANIC OrderPDFHandler orderID=%s: %v", orderID, rec) debug.PrintStack() http.Error(w, fmt.Sprintf("order pdf panic: %v", rec), http.StatusInternalServerError) } }() if orderID == "" { http.Error(w, "missing order id", http.StatusBadRequest) return } if db == nil { http.Error(w, "db not initialized", http.StatusInternalServerError) return } // Header header, err := getOrderHeaderFromDB(db, orderID) if err != nil { log.Println("header error:", err) if errors.Is(err, sql.ErrNoRows) { http.Error(w, "order not found", http.StatusNotFound) return } http.Error(w, "header not found: "+err.Error(), http.StatusInternalServerError) return } // Lines lines, err := getOrderLinesFromDB(db, orderID) if err != nil { log.Println("lines error:", err) http.Error(w, "lines not found: "+err.Error(), http.StatusInternalServerError) return } // 🔹 Satırlardan KDV bilgisi yakala (ilk pozitif orana göre) hasVat := false var vatRate float64 for _, l := range lines { if l.VatRate.Valid && l.VatRate.Float64 > 0 { hasVat = true vatRate = l.VatRate.Float64 break } } // Normalize rows := normalizeOrderLinesForPdf(lines) // PDF pdf, err := newOrderPdf() if err != nil { log.Println("pdf init error:", err) http.Error(w, "pdf init error: "+err.Error(), http.StatusInternalServerError) return } renderOrderGrid(pdf, header, rows, hasVat, vatRate) if err := pdf.Error(); err != nil { http.Error(w, "pdf render error: "+err.Error(), http.StatusInternalServerError) return } var buf bytes.Buffer if err := pdf.Output(&buf); err != nil { http.Error(w, "pdf output error: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/pdf") w.Header().Set( "Content-Disposition", fmt.Sprintf("inline; filename=\"ORDER_%s.pdf\"", header.OrderNumber), ) w.WriteHeader(http.StatusOK) _, _ = w.Write(buf.Bytes()) }) }