1524 lines
39 KiB
Go
1524 lines
39 KiB
Go
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"); 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) // 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"]
|
||
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())
|
||
})
|
||
}
|