Files
bssapp/svc/routes/order_pdf.go

1524 lines
39 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package routes
import (
"bytes"
"database/sql"
"errors"
"fmt"
"log"
"math"
"net/http"
"runtime/debug"
"sort"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/jung-kurt/gofpdf"
)
/* ===========================================================
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 parseNumericSize(v string) (int, bool) {
s := strings.TrimSpace(strings.ToUpper(v))
if s == "" {
return 0, false
}
n, err := strconv.Atoi(s)
if err != nil {
return 0, false
}
return n, true
}
func detectBedenGroupGo(bedenList []string, ana, alt string) string {
ana = safeTrimUpper(ana)
alt = safeTrimUpper(alt)
// Ürün grubu adı doğrudan ayakkabı ise öncelikli.
if strings.Contains(ana, "AYAKKABI") || strings.Contains(alt, "AYAKKABI") {
return catAyk
}
var hasYasNumeric bool
var hasAykNumeric bool
var hasPanNumeric bool
for _, b := range bedenList {
b = safeTrimUpper(b)
switch b {
case "XS", "S", "M", "L", "XL",
"2XL", "3XL", "4XL", "5XL", "6XL", "7XL":
return catGom
}
if n, ok := parseNumericSize(b); ok {
if n >= 2 && n <= 14 {
hasYasNumeric = true
}
if n >= 39 && n <= 45 {
hasAykNumeric = true
}
if n >= 38 && n <= 68 {
hasPanNumeric = true
}
}
}
if hasAykNumeric {
return catAyk
}
if strings.Contains(ana, "PANTOLON") {
return catPan
}
if hasPanNumeric {
return catPan
}
if strings.Contains(alt, "ÇOCUK") || strings.Contains(alt, "GARSON") {
return catYas
}
if hasYasNumeric {
return catYas
}
return catTak
}
func formatSizeQtyForLog(m map[string]int) string {
if len(m) == 0 {
return "{}"
}
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, k := range keys {
parts = append(parts, fmt.Sprintf("%s:%d", k, m[k]))
}
return "{" + strings.Join(parts, ", ") + "}"
}
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", "B", 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) // wrapte 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
}
/* ----------------------------------------------------
16lı 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"]
log.Printf("📄 OrderPDFHandler start orderID=%s", orderID)
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 == "" {
log.Printf("❌ OrderPDFHandler missing order id")
http.Error(w, "missing order id", http.StatusBadRequest)
return
}
if db == nil {
log.Printf("❌ OrderPDFHandler db is nil")
http.Error(w, "db not initialized", http.StatusInternalServerError)
return
}
// Header
header, err := getOrderHeaderFromDB(db, orderID)
if err != nil {
log.Printf("❌ OrderPDF header error orderID=%s: %v", orderID, 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.Printf("❌ OrderPDF lines error orderID=%s: %v", orderID, err)
http.Error(w, "lines not found: "+err.Error(), http.StatusInternalServerError)
return
}
log.Printf("📄 OrderPDF lines loaded orderID=%s lineCount=%d", orderID, len(lines))
// 🔹 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)
log.Printf("📄 OrderPDF normalized rows orderID=%s rowCount=%d", orderID, len(rows))
for i, rr := range rows {
if i >= 30 {
break
}
log.Printf(
"📄 OrderPDF row[%d] model=%s color=%s groupMain=%q groupSub=%q category=%s totalQty=%d sizeQty=%s",
i,
rr.Model,
rr.Color,
rr.GroupMain,
rr.GroupSub,
rr.Category,
rr.TotalQty,
formatSizeQtyForLog(rr.SizeQty),
)
}
// PDF
pdf, err := newOrderPdf()
if err != nil {
log.Printf("❌ OrderPDF init error orderID=%s: %v", orderID, err)
http.Error(w, "pdf init error: "+err.Error(), http.StatusInternalServerError)
return
}
renderOrderGrid(pdf, header, rows, hasVat, vatRate)
if err := pdf.Error(); err != nil {
log.Printf("❌ OrderPDF render error orderID=%s: %v", orderID, err)
http.Error(w, "pdf render error: "+err.Error(), http.StatusInternalServerError)
return
}
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
log.Printf("❌ OrderPDF output error orderID=%s: %v", orderID, err)
http.Error(w, "pdf output error: "+err.Error(), http.StatusInternalServerError)
return
}
log.Printf("✅ OrderPDF success orderID=%s bytes=%d", orderID, buf.Len())
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())
})
}