This commit is contained in:
2026-02-11 17:46:22 +03:00
commit eacfacb13b
266 changed files with 51337 additions and 0 deletions

39
svc/routes/account.go Normal file
View File

@@ -0,0 +1,39 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/internal/authz"
"bssapp-backend/queries"
"encoding/json"
"log"
"net/http"
)
func GetAccountsHandler(w http.ResponseWriter, r *http.Request) {
// ✅ AUTH (sadece login kontrolü)
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
// ✅ DEBUG (scope kontrol için faydalı)
log.Println("🔍 PIYASA CTX:", authz.GetPiyasaCodesFromCtx(r.Context()))
// ✅ QUERY
accounts, err := queries.GetAccounts(r.Context())
if err != nil {
log.Println("❌ GetAccounts error:", err)
http.Error(w, "db error", http.StatusInternalServerError)
return
}
// ✅ RESPONSE
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if err := json.NewEncoder(w).Encode(accounts); err != nil {
log.Println("❌ JSON encode error:", err)
return
}
}

126
svc/routes/activitylogs.go Normal file
View File

@@ -0,0 +1,126 @@
package routes
import (
"bssapp-backend/repository"
"database/sql"
"encoding/json"
"net/http"
"strconv"
"time"
)
type ActivityLogQuery struct {
Username string
RoleCode string
ActionCategory string
ActionType string
ActionTarget string
Success *bool
DateFrom *time.Time
DateTo *time.Time
Page int
Limit int
}
func AdminActivityLogsHandler(pgDB *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
q := repository.ActivityLogQuery{}
// pagination
if v := r.URL.Query().Get("page"); v != "" {
if i, err := strconv.Atoi(v); err == nil {
q.Page = i
}
}
if v := r.URL.Query().Get("limit"); v != "" {
if i, err := strconv.Atoi(v); err == nil {
q.Limit = i
}
} else {
// limit yoksa unlimited
q.Limit = 0
}
// filters
q.Username = r.URL.Query().Get("username")
q.RoleCode = r.URL.Query().Get("role_code")
q.ActionCategory = r.URL.Query().Get("action_category")
q.ActionType = r.URL.Query().Get("action_type")
q.ActionTarget = r.URL.Query().Get("action_target")
// success
if v := r.URL.Query().Get("success"); v != "" {
if v == "true" || v == "1" {
b := true
q.Success = &b
} else if v == "false" || v == "0" {
b := false
q.Success = &b
}
}
// status range
if v := r.URL.Query().Get("status_min"); v != "" {
if i, err := strconv.Atoi(v); err == nil {
q.StatusMin = &i
}
}
if v := r.URL.Query().Get("status_max"); v != "" {
if i, err := strconv.Atoi(v); err == nil {
q.StatusMax = &i
}
}
// date range (ISO: 2026-01-03T00:00:00Z veya 2026-01-03)
parseDate := func(s string) (*time.Time, error) {
if s == "" {
return nil, nil
}
// önce RFC3339 dene
if t, err := time.Parse(time.RFC3339, s); err == nil {
return &t, nil
}
// sonra sadece tarih
if t, err := time.Parse("2006-01-02", s); err == nil {
tt := t
return &tt, nil
}
return nil, http.ErrNotSupported
}
if v := r.URL.Query().Get("date_from"); v != "" {
t, err := parseDate(v)
if err != nil {
http.Error(w, "date_from format hatalı", http.StatusBadRequest)
return
}
q.DateFrom = t
}
if v := r.URL.Query().Get("date_to"); v != "" {
t, err := parseDate(v)
if err != nil {
http.Error(w, "date_to format hatalı", http.StatusBadRequest)
return
}
q.DateTo = t
}
res, err := repository.ListActivityLogs(r.Context(), pgDB, q)
if err != nil {
http.Error(w, "Loglar alınamadı", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"page": q.Page,
"limit": q.Limit,
"total": res.Total,
"items": res.Items,
})
}
}

View File

@@ -0,0 +1,61 @@
package routes
import (
"log"
"net/http"
"strconv"
"bssapp-backend/auth"
"bssapp-backend/internal/authz"
"bssapp-backend/middlewares"
"github.com/gorilla/mux"
)
// =====================================================
// 🔐 ADMIN — PIYASA CACHE SYNC
// =====================================================
// POST /api/admin/users/{id}/piyasa-sync
func AdminSyncUserPiyasaHandler(w http.ResponseWriter, r *http.Request) {
// --------------------------------------------------
// 🔐 AUTH
// --------------------------------------------------
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", 401)
return
}
// --------------------------------------------------
// 🆔 USER ID PARAM
// --------------------------------------------------
vars := mux.Vars(r)
idStr := vars["id"]
targetID, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
http.Error(w, "invalid user id", http.StatusBadRequest)
return
}
// --------------------------------------------------
// 🧹 CACHE CLEAR
// --------------------------------------------------
authz.ClearPiyasaCache(int(targetID))
middlewares.ClearAuthzScopeCacheForUser(targetID)
log.Printf(
"🔄 ADMIN PIYASA SYNC | admin=%d target=%d",
claims.ID,
targetID,
)
// --------------------------------------------------
// ✅ OK
// --------------------------------------------------
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok"}`))
}

View File

@@ -0,0 +1,104 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/internal/auditlog"
"bssapp-backend/repository"
"database/sql"
"encoding/json"
"github.com/gorilla/mux"
"golang.org/x/crypto/bcrypt"
"net/http"
"strconv"
)
type AdminResetPasswordRequest struct {
Password string `json:"password"` // opsiyonel
}
func AdminResetPasswordHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
// ---------------------------------------------------
// 1⃣ USER ID
// ---------------------------------------------------
idStr := mux.Vars(r)["id"]
userID, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || userID <= 0 {
http.Error(w, "invalid user id", http.StatusBadRequest)
return
}
// ---------------------------------------------------
// 2⃣ PAYLOAD (opsiyonel)
// ---------------------------------------------------
var req AdminResetPasswordRequest
_ = json.NewDecoder(r.Body).Decode(&req)
// parola verilmediyse backend üretir
password := req.Password
if password == "" {
password = "Temp123!" // 👉 istersek random generator ekleriz
}
// ---------------------------------------------------
// 3⃣ HASH
// ---------------------------------------------------
hash, err := bcrypt.GenerateFromPassword(
[]byte(password),
bcrypt.DefaultCost,
)
if err != nil {
http.Error(w, "password hash error", http.StatusInternalServerError)
return
}
// ---------------------------------------------------
// 4⃣ UPDATE mk_dfusr
// ---------------------------------------------------
_, err = db.Exec(`
UPDATE mk_dfusr
SET
password_hash = $1,
force_password_change = true,
password_updated_at = NOW(),
updated_at = NOW()
WHERE id = $2
`, string(hash), userID)
if err != nil {
http.Error(w, "password reset failed", http.StatusInternalServerError)
return
}
// ---------------------------------------------------
// 5⃣ REFRESH TOKEN REVOKE
// ---------------------------------------------------
_ = repository.
NewRefreshTokenRepository(db).
RevokeAllForUser(userID)
// ---------------------------------------------------
// 6⃣ AUDIT
// ---------------------------------------------------
auditlog.Write(auditlog.ActivityLog{
ActionType: "ADMIN_PASSWORD_RESET",
ActionCategory: "security",
ActionTarget: "mk_dfusr.id",
IsSuccess: true,
})
// ---------------------------------------------------
// 7⃣ RESPONSE
// ---------------------------------------------------
_ = json.NewEncoder(w).Encode(map[string]any{
"success": true,
})
}
}

View File

@@ -0,0 +1,60 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/ctxkeys"
"bssapp-backend/internal/auditlog"
"bssapp-backend/permissions"
"bssapp-backend/repository"
"database/sql"
"encoding/json"
)
// auditLogFromRequest
// routes içinden çağrılır
// auditLogFromRequest
// routes içinden çağrılır
func auditLogFromRequest(
ctx any,
db *sql.DB,
actionType string,
meta map[string]any,
) {
al := auditlog.ActivityLog{
ActionType: actionType,
ActionCategory: "ADMIN",
IsSuccess: true,
}
// JWT → identity
if c, ok := ctx.(interface {
Value(any) any
}); ok {
if claims, ok := c.Value(ctxkeys.UserContextKey).(*auth.Claims); ok && claims != nil {
// ✅ TEK KİMLİK
al.DfUsrID = claims.ID
al.Username = claims.Username
al.RoleCode = claims.RoleCode
// 🔗 MULTI ROLE → ADMIN CHECK
roles, err := repository.GetUserRolesByUserID(db, claims.ID)
if err == nil {
_, isAdmin := permissions.ResolveEffectiveRoles(roles)
if isAdmin {
al.RoleCode = "admin"
}
}
}
}
// meta → description
if meta != nil {
if b, err := json.Marshal(meta); err == nil {
al.Description = string(b)
}
}
auditlog.Write(al)
}

View File

@@ -0,0 +1,92 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/internal/security"
"bssapp-backend/repository"
"database/sql"
"encoding/json"
"net/http"
"time"
)
func setRefreshCookie(w http.ResponseWriter, plain string, exp time.Time) {
http.SetCookie(w, &http.Cookie{
Name: "mk_refresh",
Value: plain,
Path: "/",
Expires: exp,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: false, // prod: true
})
}
func AuthRefreshHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
// 1) refresh cookie
c, err := r.Cookie("mk_refresh")
if err != nil || c.Value == "" {
http.Error(w, "refresh token missing", http.StatusUnauthorized)
return
}
hash := security.HashRefreshToken(c.Value)
rtRepo := repository.NewRefreshTokenRepository(db)
// 2) validate + consume
mkUserID, err := rtRepo.ConsumeValid(hash)
if err != nil {
http.Error(w, "refresh token invalid", http.StatusUnauthorized)
return
}
// 3) rotate
newPlain, newHash, err := security.GenerateRefreshToken()
if err != nil {
http.Error(w, "refresh gen failed", http.StatusInternalServerError)
return
}
refreshExp := time.Now().Add(14 * 24 * time.Hour)
if err := rtRepo.IssueRefreshToken(mkUserID, newHash, refreshExp); err != nil {
http.Error(w, "refresh store failed", http.StatusInternalServerError)
return
}
setRefreshCookie(w, newPlain, refreshExp)
// 4) mk user reload
mkRepo := repository.NewMkUserRepository(db)
mkUser, err := mkRepo.GetByID(mkUserID)
if err != nil || !mkUser.IsActive {
http.Error(w, "user invalid", http.StatusUnauthorized)
return
}
if mkUser.ForcePasswordChange {
http.Error(w, "password change required", http.StatusForbidden)
return
}
// 5) new access token
claims := auth.BuildClaimsFromUser(mkUser, 15*time.Minute)
token, err := auth.GenerateToken(
claims,
mkUser.Username,
mkUser.ForcePasswordChange,
)
if err != nil {
http.Error(w, "access token gen failed", http.StatusInternalServerError)
return
}
// 6) response
_ = json.NewEncoder(w).Encode(map[string]any{
"success": true,
"token": token,
})
}
}

View File

@@ -0,0 +1,57 @@
package routes
import (
"encoding/json"
"log"
"net/http"
"bssapp-backend/auth"
"bssapp-backend/queries"
)
func GetCustomerListHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
// --------------------------------------------------
// 🔐 CLAIMS (DEBUG / TRACE)
// --------------------------------------------------
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
log.Printf(
"📥 /api/customers | user=%d admin=%v",
claims.ID,
claims.IsAdmin(),
)
// --------------------------------------------------
// 🗄️ QUERY (CONTEXT TAŞIYOR)
// --------------------------------------------------
list, err := queries.GetCustomerList(r.Context())
if err != nil {
log.Println("❌ Customer list error:", err)
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
return
}
// --------------------------------------------------
// 📤 JSON OUTPUT
// --------------------------------------------------
if err := json.NewEncoder(w).Encode(list); err != nil {
log.Printf("❌ JSON encode error: %v", err)
return
}
// --------------------------------------------------
// 📊 RESULT LOG
// --------------------------------------------------
log.Printf(
"✅ Customer list DONE | user=%d | resultCount=%d",
claims.ID,
len(list),
)
}

View File

@@ -0,0 +1,169 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/internal/auditlog"
"bssapp-backend/internal/security"
"bssapp-backend/models"
"bssapp-backend/repository"
"database/sql"
"encoding/json"
"log"
"net/http"
"time"
"golang.org/x/crypto/bcrypt"
)
func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
// --------------------------------------------------
// 1⃣ JWT CLAIMS
// --------------------------------------------------
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
// --------------------------------------------------
// 2⃣ PAYLOAD
// --------------------------------------------------
var req struct {
CurrentPassword string `json:"current_password"`
NewPassword string `json:"new_password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid payload", http.StatusBadRequest)
return
}
if req.CurrentPassword == "" || req.NewPassword == "" {
http.Error(w, "password fields required", http.StatusUnprocessableEntity)
return
}
// --------------------------------------------------
// 3⃣ LOAD USER (mk_dfusr)
// --------------------------------------------------
var currentHash string
err := db.QueryRow(`
SELECT password_hash
FROM mk_dfusr
WHERE id = $1
`, claims.ID).Scan(&currentHash)
if err != nil || currentHash == "" {
http.Error(w, "user not found", http.StatusUnauthorized)
return
}
// --------------------------------------------------
// 4⃣ CURRENT PASSWORD CHECK
// --------------------------------------------------
if bcrypt.CompareHashAndPassword(
[]byte(currentHash),
[]byte(req.CurrentPassword),
) != nil {
http.Error(w, "mevcut şifre hatalı", http.StatusUnauthorized)
return
}
// --------------------------------------------------
// 5⃣ PASSWORD POLICY
// --------------------------------------------------
if err := security.ValidatePassword(req.NewPassword); err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
// --------------------------------------------------
// 6⃣ HASH NEW PASSWORD
// --------------------------------------------------
hash, err := bcrypt.GenerateFromPassword(
[]byte(req.NewPassword),
bcrypt.DefaultCost,
)
if err != nil {
http.Error(w, "password hash error", http.StatusInternalServerError)
return
}
// --------------------------------------------------
// 7⃣ UPDATE mk_dfusr
// --------------------------------------------------
_, err = db.Exec(`
UPDATE mk_dfusr
SET
password_hash = $1,
force_password_change = false,
password_updated_at = NOW(),
updated_at = NOW()
WHERE id = $2
`, string(hash), claims.ID)
if err != nil {
http.Error(w, "password update failed", http.StatusInternalServerError)
return
}
// --------------------------------------------------
// 8⃣ REFRESH TOKEN REVOKE
// --------------------------------------------------
_ = repository.
NewRefreshTokenRepository(db).
RevokeAllForUser(claims.ID)
// --------------------------------------------------
// 9⃣ NEW JWT (TEK DOĞRU YOL)
// --------------------------------------------------
newClaims := auth.BuildClaimsFromUser(
&models.MkUser{
ID: claims.ID,
Username: claims.Username,
RoleCode: claims.RoleCode,
V3Username: claims.V3Username,
V3UserGroup: claims.V3UserGroup,
SessionID: claims.SessionID,
ForcePasswordChange: false,
},
15*time.Minute,
)
newToken, err := auth.GenerateToken(
newClaims,
claims.Username,
false,
)
if err != nil {
http.Error(w, "token generation failed", http.StatusInternalServerError)
return
}
// --------------------------------------------------
// 🔟 AUDIT
// --------------------------------------------------
auditlog.ForcePasswordChangeCompleted(
r.Context(),
claims.ID,
"self_change",
)
// --------------------------------------------------
// 1⃣1⃣ RESPONSE
// --------------------------------------------------
_ = json.NewEncoder(w).Encode(map[string]any{
"token": newToken,
"user": map[string]any{
"id": claims.ID,
"username": claims.Username,
"force_password_change": false,
},
})
log.Printf("✅ FIRST-PASS claims user=%d role=%s", claims.ID, claims.RoleCode)
}
}

733
svc/routes/login.go Normal file
View File

@@ -0,0 +1,733 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/internal/auditlog"
"bssapp-backend/models"
"bssapp-backend/queries"
"bssapp-backend/repository"
"bssapp-backend/services"
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
"golang.org/x/crypto/bcrypt"
)
/* ======================================================
🔐 LOGIN
====================================================== */
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
func LoginHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
// --------------------------------------------------
// 0⃣ REQUEST
// --------------------------------------------------
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Geçersiz JSON", http.StatusBadRequest)
return
}
login := strings.TrimSpace(req.Username)
pass := req.Password // ⚠️ TRIM YAPMA
if login == "" || pass == "" {
http.Error(w, "Kullanıcı adı veya parola hatalı", http.StatusUnauthorized)
return
}
mkRepo := repository.NewMkUserRepository(db)
// ==================================================
// 1⃣ mk_dfusr ÖNCELİKLİ
// ==================================================
mkUser, err := mkRepo.GetByUsername(login)
if err == nil {
log.Println("🧪 MK USER FROM DB")
log.Printf("🧪 ID=%d role_id=%d role_code='%s' depts=%v",
mkUser.ID,
mkUser.RoleID,
mkUser.RoleCode,
mkUser.DepartmentCodes,
)
}
log.Printf(
"🔎 LOGIN DEBUG | mk_user_found=%v err=%v hash_len=%d",
err == nil,
err,
func() int {
if err == nil {
return len(strings.TrimSpace(mkUser.PasswordHash))
}
return 0
}(),
)
if err == nil {
// mk_dfusr authoritative
if strings.TrimSpace(mkUser.PasswordHash) != "" {
if bcrypt.CompareHashAndPassword(
[]byte(mkUser.PasswordHash),
[]byte(pass),
) != nil {
http.Error(w, "Kullanıcı adı veya parola hatalı", http.StatusUnauthorized)
return
}
_ = mkRepo.TouchLastLogin(mkUser.ID)
writeLoginResponse(w, db, mkUser)
return
}
// password_hash boşsa legacy fallback
} else if err != repository.ErrMkUserNotFound {
log.Println("❌ mk_dfusr lookup error:", err)
http.Error(w, "Giriş yapılamadı", http.StatusInternalServerError)
return
}
// ==================================================
// 2⃣ dfusr FALLBACK (LEGACY)
// ==================================================
log.Println("🟡 LEGACY LOGIN PATH:", login)
legacyRepo := repository.NewUserRepository(db)
legacyUser, err := legacyRepo.GetLegacyUserForLogin(login)
if err != nil || !legacyUser.IsActive {
http.Error(w, "Kullanıcı adı veya parola hatalı", http.StatusUnauthorized)
return
}
log.Printf(
"🔎 LOGIN DEBUG | legacy_upass_len=%d prefix=%s",
len(strings.TrimSpace(legacyUser.Upass)),
func() string {
if len(legacyUser.Upass) >= 4 {
return legacyUser.Upass[:4]
}
return legacyUser.Upass
}(),
)
if !services.CheckPasswordWithLegacy(legacyUser, pass) {
http.Error(w, "Kullanıcı adı veya parola hatalı", http.StatusUnauthorized)
return
}
// ==================================================
// 3⃣ MIGRATION (dfusr → mk_dfusr)
// ==================================================
newHash, err := bcrypt.GenerateFromPassword(
[]byte(pass),
bcrypt.DefaultCost,
)
if err != nil {
http.Error(w, "Şifre üretilemedi", http.StatusInternalServerError)
return
}
mkUser, err = mkRepo.CreateFromLegacy(legacyUser, string(newHash))
if err != nil {
log.Println("❌ CREATE_FROM_LEGACY FAILED:", err)
http.Error(w, "Kullanıcı migrate edilemedi", http.StatusInternalServerError)
return
}
// 🔥 KRİTİK: TOKEN GUARD İÇİN GARANTİ
mkUser.ForcePasswordChange = true
auditlog.Write(auditlog.ActivityLog{
ActionType: "LEGACY_USER_MIGRATED",
ActionCategory: "security",
Description: "dfusr -> mk_dfusr on login",
IsSuccess: true,
})
// ==================================================
// 4⃣ SUCCESS
// ==================================================
writeLoginResponse(w, db, mkUser)
}
}
// ======================================================
// 🔑 LOGIN RESPONSE
// ======================================================
func writeLoginResponse(w http.ResponseWriter, db *sql.DB, user *models.MkUser) {
// 🔥 ROLE GARANTİSİ
if user.RoleID == 0 {
_ = db.QueryRow(`
SELECT dfrole_id
FROM dfrole_usr
WHERE dfusr_id = $1
LIMIT 1
`, user.ID).Scan(&user.RoleID)
}
if user.RoleCode == "" && user.RoleID > 0 {
_ = db.QueryRow(`
SELECT code
FROM dfrole
WHERE id = $1
`, user.RoleID).Scan(&user.RoleCode)
}
log.Println("🧪 LOGIN RESPONSE USER DEBUG")
log.Printf("🧪 user.ID = %d", user.ID)
log.Printf("🧪 user.Username = %s", user.Username)
log.Printf("🧪 user.RoleID = %d", user.RoleID)
log.Printf("🧪 user.RoleCode = '%s'", user.RoleCode)
log.Printf("🧪 user.IsActive = %v", user.IsActive)
permRepo := repository.NewPermissionRepository(db)
perms, _ := permRepo.GetPermissionsByRoleID(user.RoleID)
// ✅ CLAIMS BUILD
claims := auth.BuildClaimsFromUser(user, 15*time.Minute)
token, err := auth.GenerateToken(
claims,
user.Username,
user.ForcePasswordChange,
)
if err != nil {
http.Error(w, "Token üretilemedi", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(map[string]any{
"token": token,
"user": map[string]any{
"id": user.ID,
"username": user.Username,
"email": user.Email,
"is_active": user.IsActive,
"role_id": user.RoleID,
"role_code": user.RoleCode,
"force_password_change": user.ForcePasswordChange,
"v3_username": user.V3Username,
"v3_usergroup": user.V3UserGroup,
},
"permissions": perms,
})
}
/* ======================================================
🔎 LOOKUPS (aynen korunuyor)
====================================================== */
type LookupOption struct {
Value string `json:"value"`
Label string `json:"label"`
}
func GetRoleLookupRoute(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query(queries.GetRoleLookup)
if err != nil {
http.Error(w, "role lookup error", http.StatusInternalServerError)
return
}
defer rows.Close()
var list []LookupOption
for rows.Next() {
var o LookupOption
if err := rows.Scan(&o.Value, &o.Label); err == nil {
list = append(list, o)
}
}
_ = json.NewEncoder(w).Encode(list)
}
}
func GetDepartmentLookupRoute(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query(queries.GetDepartmentLookup)
if err != nil {
http.Error(w, "department lookup error", http.StatusInternalServerError)
return
}
defer rows.Close()
var list []LookupOption
for rows.Next() {
var o LookupOption
if err := rows.Scan(&o.Value, &o.Label); err == nil {
list = append(list, o)
}
}
_ = json.NewEncoder(w).Encode(list)
}
}
func GetPiyasaLookupRoute(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query(queries.GetPiyasaLookup)
if err != nil {
http.Error(w, "piyasa lookup error", http.StatusInternalServerError)
return
}
defer rows.Close()
var list []LookupOption
for rows.Next() {
var o LookupOption
if err := rows.Scan(&o.Value, &o.Label); err == nil {
list = append(list, o)
}
}
_ = json.NewEncoder(w).Encode(list)
}
}
// ======================================================
// 🧾 NEBIM USER LOOKUP
// GET /api/lookups/nebim-users
// ======================================================
func GetNebimUserLookupRoute(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query(queries.GetNebimUserLookup)
if err != nil {
http.Error(w, "nebim lookup error", http.StatusInternalServerError)
return
}
defer rows.Close()
var list []map[string]string
for rows.Next() {
var v, l, g string
if err := rows.Scan(&v, &l, &g); err == nil {
list = append(list, map[string]string{
"value": v,
"label": l,
"user_group_code": g,
})
}
}
_ = json.NewEncoder(w).Encode(list)
}
}
func UserCreateRoute(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
var payload models.UserWrite
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "Geçersiz payload", http.StatusBadRequest)
return
}
if payload.Code == "" {
http.Error(w, "Kullanıcı kodu zorunludur", http.StatusUnprocessableEntity)
return
}
tx, err := db.Begin()
if err != nil {
http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError)
return
}
defer tx.Rollback()
var newID int64
err = tx.QueryRow(`
INSERT INTO mk_dfusr (
code,
is_active,
full_name,
email,
mobile,
address,
force_password_change,
last_updated_date
)
VALUES ($1,$2,$3,$4,$5,$6,true,NOW())
RETURNING id
`,
payload.Code,
payload.IsActive,
payload.FullName,
payload.Email,
payload.Mobile,
payload.Address,
).Scan(&newID)
if err != nil {
log.Println("USER INSERT ERROR:", err)
http.Error(w, "Kullanıcı oluşturulamadı", http.StatusInternalServerError)
return
}
// ROLES
for _, role := range payload.Roles {
_, _ = tx.Exec(queries.InsertUserRole, newID, role)
}
// DEPARTMENTS
for _, d := range payload.Departments {
_, _ = tx.Exec(queries.InsertUserDepartment, newID, d.Code)
}
// PIYASALAR
for _, p := range payload.Piyasalar {
_, _ = tx.Exec(queries.InsertUserPiyasa, newID, p.Code)
}
// NEBIM
for _, n := range payload.NebimUsers {
_, _ = tx.Exec(queries.InsertUserNebim, newID, n.Username)
}
if err := tx.Commit(); err != nil {
http.Error(w, "Commit başarısız", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(map[string]any{
"success": true,
"id": newID,
})
}
}
// ======================================================
// 🔐 ROLES LIST
// GET /api/roles
// ======================================================
func GetRolesHandler(db *sql.DB) http.HandlerFunc {
type RoleRow struct {
ID int64 `json:"id"`
Code string `json:"code"`
Title string `json:"title"`
}
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
rows, err := db.Query(queries.GetRoles)
if err != nil {
http.Error(w, "roles query error", http.StatusInternalServerError)
return
}
defer rows.Close()
var list []RoleRow
for rows.Next() {
var x RoleRow
if err := rows.Scan(&x.ID, &x.Code, &x.Title); err == nil {
list = append(list, x)
}
}
_ = json.NewEncoder(w).Encode(list)
}
}
// ======================================================
// 🏢 DEPARTMENTS LIST
// GET /api/departments
// ======================================================
func GetDepartmentsHandler(db *sql.DB) http.HandlerFunc {
type DeptRow struct {
ID int64 `json:"id"`
Code string `json:"code"`
Title string `json:"title"`
}
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
rows, err := db.Query(queries.GetDepartments)
if err != nil {
http.Error(w, "departments query error", http.StatusInternalServerError)
return
}
defer rows.Close()
var list []DeptRow
for rows.Next() {
var x DeptRow
if err := rows.Scan(&x.ID, &x.Code, &x.Title); err == nil {
list = append(list, x)
}
}
_ = json.NewEncoder(w).Encode(list)
}
}
// ======================================================
// 🌍 PIYASALAR LIST
// GET /api/piyasalar
// ======================================================
func GetPiyasalarHandler(db *sql.DB) http.HandlerFunc {
type PiyRow struct {
Code string `json:"code"`
Title string `json:"title"`
}
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
rows, err := db.Query(queries.GetPiyasalar)
if err != nil {
http.Error(w, "piyasalar query error", http.StatusInternalServerError)
return
}
defer rows.Close()
var list []PiyRow
for rows.Next() {
var x PiyRow
if err := rows.Scan(&x.Code, &x.Title); err == nil {
list = append(list, x)
}
}
_ = json.NewEncoder(w).Encode(list)
}
}
// ======================================================
// 🔗 ROLE → DEPARTMENTS UPDATE
// POST /api/roles/{id}/departments
// body: { "codes": ["D01","D02"] }
// ======================================================
type CodesPayload struct {
Codes []string `json:"codes"`
}
func UpdateRoleDepartmentsHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
roleIDStr := mux.Vars(r)["id"]
roleID, err := strconv.ParseInt(roleIDStr, 10, 64)
if err != nil || roleID <= 0 {
http.Error(w, "Geçersiz role id", http.StatusBadRequest)
return
}
var payload CodesPayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "Geçersiz payload", http.StatusBadRequest)
return
}
tx, err := db.Begin()
if err != nil {
http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError)
return
}
defer tx.Rollback()
// reset
if _, err := tx.Exec(queries.DeleteRoleDepartments, roleID); err != nil {
http.Error(w, "Role departments silinemedi", http.StatusInternalServerError)
return
}
// insert new
for _, code := range payload.Codes {
code = strings.TrimSpace(code)
if code == "" {
continue
}
if _, err := tx.Exec(queries.InsertRoleDepartment, roleID, code); err != nil {
http.Error(w, "Role department eklenemedi", http.StatusInternalServerError)
return
}
}
if err := tx.Commit(); err != nil {
http.Error(w, "Commit başarısız", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"success": true})
}
}
// ======================================================
// 🔗 ROLE → PIYASALAR UPDATE
// POST /api/roles/{id}/piyasalar
// body: { "codes": ["TR","EU"] } (piyasa_code list)
// ======================================================
func UpdateRolePiyasalarHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
roleIDStr := mux.Vars(r)["id"]
roleID, err := strconv.ParseInt(roleIDStr, 10, 64)
if err != nil || roleID <= 0 {
http.Error(w, "Geçersiz role id", http.StatusBadRequest)
return
}
var payload CodesPayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "Geçersiz payload", http.StatusBadRequest)
return
}
tx, err := db.Begin()
if err != nil {
http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError)
return
}
defer tx.Rollback()
// reset
if _, err := tx.Exec(queries.DeleteRolePiyasalar, roleID); err != nil {
http.Error(w, "Role piyasalar silinemedi", http.StatusInternalServerError)
return
}
// insert new
for _, code := range payload.Codes {
code = strings.TrimSpace(code)
if code == "" {
continue
}
if _, err := tx.Exec(queries.InsertRolePiyasa, roleID, code); err != nil {
http.Error(w, "Role piyasa eklenemedi", http.StatusInternalServerError)
return
}
}
if err := tx.Commit(); err != nil {
http.Error(w, "Commit başarısız", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"success": true})
}
}
// ======================================================
// 👤 USER → ROLES UPDATE
// POST /api/users/{id}/roles
// body: { "codes": ["admin","user"] } (dfrole.code list)
// ======================================================
func UpdateUserRolesHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
// ---------- CLAIMS ----------
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || !claims.IsAdmin() {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
// ---------- TARGET USER ----------
userIDStr := mux.Vars(r)["id"]
userID, err := strconv.ParseInt(userIDStr, 10, 64)
if err != nil || userID <= 0 {
http.Error(w, "Geçersiz user id", http.StatusBadRequest)
return
}
// ---------- PAYLOAD ----------
var payload CodesPayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "Geçersiz payload", http.StatusBadRequest)
return
}
// ---------- BEFORE (ROLE LIST) ----------
oldRoles, _ := repository.GetUserRolesByUserID(db, userID)
// ---------- TX ----------
tx, err := db.Begin()
if err != nil {
http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError)
return
}
defer tx.Rollback()
// reset
if _, err := tx.Exec(queries.DeleteUserRoles, userID); err != nil {
http.Error(w, "User roles silinemedi", http.StatusInternalServerError)
return
}
// insert new
for _, roleCode := range payload.Codes {
roleCode = strings.TrimSpace(roleCode)
if roleCode == "" {
continue
}
if _, err := tx.Exec(queries.InsertUserRole, userID, roleCode); err != nil {
http.Error(w, "User role eklenemedi", http.StatusInternalServerError)
return
}
}
if err := tx.Commit(); err != nil {
http.Error(w, "Commit başarısız", http.StatusInternalServerError)
return
}
// ---------- AFTER (ROLE LIST) ----------
newRoles, _ := repository.GetUserRolesByUserID(db, userID)
// ---------- AUDIT ----------
auditlog.Enqueue(r.Context(), auditlog.ActivityLog{
ActionType: "role_change",
ActionCategory: "user_admin",
ActionTarget: fmt.Sprintf("/api/users/%d/roles", userID),
Description: "user roles updated",
Username: claims.Username,
RoleCode: claims.RoleCode,
DfUsrID: int64(claims.ID),
TargetDfUsrID: userID,
ChangeBefore: map[string]any{
"roles": oldRoles,
},
ChangeAfter: map[string]any{
"roles": newRoles,
},
IsSuccess: true,
})
_ = json.NewEncoder(w).Encode(map[string]any{
"success": true,
})
}
}

View File

@@ -0,0 +1,91 @@
package routes
import (
"bssapp-backend/models"
"bssapp-backend/queries"
"database/sql"
"fmt"
"net/http"
"time"
"github.com/xuri/excelize/v2"
)
func OrderListExcelRoute(db *sql.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
search := r.URL.Query().Get("search")
currAcc := r.URL.Query().Get("CurrAccCode")
orderDate := r.URL.Query().Get("OrderDate")
rows, err := queries.GetOrderListExcel(db, search, currAcc, orderDate)
if err != nil {
http.Error(w, "Veritabanı hatası", 500)
return
}
defer rows.Close()
f := excelize.NewFile()
sheet := "Orders"
f.SetSheetName("Sheet1", sheet)
headers := []string{
"Sipariş No", "Tarih", "Cari Kod", "Cari Adı",
"Temsilci", "Piyasa", "Onay Tarihi", "PB",
"Tutar", "Tutar (USD)", "Açıklama",
}
for i, h := range headers {
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
f.SetCellValue(sheet, cell, h)
}
rowIdx := 2
for rows.Next() {
var o models.OrderList
_ = rows.Scan(
&o.OrderHeaderID,
&o.OrderNumber,
&o.OrderDate,
&o.CurrAccCode,
&o.CurrAccDescription,
&o.MusteriTemsilcisi,
&o.Piyasa,
&o.CreditableConfirmedDate,
&o.DocCurrencyCode,
&o.TotalAmount,
&o.TotalAmountUSD,
&o.IsCreditableConfirmed,
&o.Description,
&o.ExchangeRateUSD,
)
f.SetSheetRow(sheet, fmt.Sprintf("A%d", rowIdx), &[]interface{}{
o.OrderNumber,
o.OrderDate,
o.CurrAccCode,
o.CurrAccDescription,
o.MusteriTemsilcisi,
o.Piyasa,
o.CreditableConfirmedDate,
o.DocCurrencyCode,
o.TotalAmount,
o.TotalAmountUSD,
o.Description,
})
rowIdx++
}
filename := fmt.Sprintf(
"order_list_%s.xlsx",
time.Now().Format("2006-01-02_15-04"),
)
w.Header().Set("Content-Type",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
w.Header().Set("Content-Disposition",
"attachment; filename="+filename)
_ = f.Write(w)
})
}

1387
svc/routes/order_pdf.go Normal file
View File

@@ -0,0 +1,1387 @@
package routes
import (
"bytes"
"database/sql"
"fmt"
"github.com/gorilla/mux"
"github.com/jung-kurt/gofpdf"
"log"
"math"
"net/http"
"os"
"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 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 {
pdf := gofpdf.New("L", "mm", "A4", "")
pdf.SetMargins(10, 10, 10)
pdf.SetAutoPageBreak(false, 12)
// UTF8 fontlar
pdf.AddUTF8Font("dejavu", "", "fonts/DejaVuSans.ttf")
pdf.AddUTF8Font("dejavu-b", "", "fonts/DejaVuSans-Bold.ttf")
// 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
}
/* ===========================================================
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
err := row.Scan(
&h.OrderHeaderID,
&h.OrderNumber,
&h.CurrAccCode,
&h.CurrAccName,
&h.DocCurrency,
&orderDate,
&h.Description,
&h.InternalDesc,
&h.OfficeCode,
&h.CreatedUser,
&h.CustomerRep, // 🆕 buradan geliyor
)
if err != nil {
return nil, err
}
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
---------------------------------------------------- */
logo := "./public/Baggi-Tekstil-A.s-Logolu.jpeg"
if _, err := os.Stat(logo); err == nil {
pdf.ImageOptions(logo, 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
}
// Yeni: açıklama genişliği = sol + sağ
descW := layout.ColDescW
lines := pdf.SplitLines([]byte(desc), descW-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"]
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)
http.Error(w, "header not found", http.StatusInternalServerError)
return
}
// Lines
lines, err := getOrderLinesFromDB(db, orderID)
if err != nil {
log.Println("lines error:", err)
http.Error(w, "lines not found", 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 := newOrderPdf()
renderOrderGrid(pdf, header, rows, hasVat, vatRate)
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())
})
}

View File

@@ -0,0 +1,45 @@
package routes
import (
"bssapp-backend/models"
"bssapp-backend/queries"
"database/sql"
"encoding/json"
"fmt"
"net/http"
)
type validateOrderRequest struct {
Header models.OrderHeader `json:"header"`
Lines []models.OrderDetail `json:"lines"`
}
type validateOrderResponse struct {
OK bool `json:"ok"`
Invalid []models.InvalidVariant `json:"invalid"`
}
func ValidateOrderHandler(mssql *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req validateOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, fmt.Sprintf("JSON parse hatası: %v", err), http.StatusBadRequest)
return
}
invalid, err := queries.ValidateOrderVariants(mssql, req.Lines)
if err != nil {
http.Error(w, fmt.Sprintf("Validate error: %v", err), http.StatusInternalServerError)
return
}
resp := validateOrderResponse{
OK: len(invalid) == 0,
Invalid: invalid,
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(resp)
}
}

View File

@@ -0,0 +1,89 @@
package routes
import (
"bssapp-backend/db"
"bssapp-backend/queries"
"context"
"encoding/json"
"log"
"net/http"
"time"
)
// ✅ GET /api/order-inventory?code=...&color=...&color2=...
func GetOrderInventoryHandler(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
color := r.URL.Query().Get("color")
color2 := r.URL.Query().Get("color2")
if code == "" {
http.Error(w, "Eksik parametre: code gerekli", http.StatusBadRequest)
return
}
// 🔧 Normalize eksik renk parametreleri
if color == "" {
color = " "
}
if color2 == "" {
color2 = " "
}
log.Printf("🎨 [ORDERINV] İstek alındı -> code=%q | color=%q | color2=%q", code, color, color2)
// ✅ MSSQL bağlantısını ve timeout'u oluştur
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
rows, err := db.MssqlDB.QueryContext(ctx, queries.GetOrderInventory, code, color, color2)
if err != nil {
log.Printf("❌ [ORDERINV] SQL çalıştırılamadı: %v", err)
http.Error(w, "SQL hatası: "+err.Error(), http.StatusInternalServerError)
return
}
defer func() {
if cerr := rows.Close(); cerr != nil {
log.Printf("⚠️ [ORDERINV] rows.Close() hatası: %v", cerr)
}
}()
type Row struct {
UrunKodu string `json:"UrunKodu"`
RenkKodu string `json:"RenkKodu"`
RenkAciklamasi string `json:"RenkAciklamasi"`
Beden string `json:"Beden"`
Yaka string `json:"Yaka"`
KullanilabilirAdet float64 `json:"KullanilabilirAdet"`
}
var list []Row
for rows.Next() {
var r Row
if err := rows.Scan(
&r.UrunKodu,
&r.RenkKodu,
&r.RenkAciklamasi,
&r.Beden,
&r.Yaka,
&r.KullanilabilirAdet,
); err != nil {
log.Printf("⚠️ [ORDERINV] Scan hatası: %v", err)
continue
}
list = append(list, r)
}
if err := rows.Err(); err != nil {
log.Printf("❌ [ORDERINV] Rows hatası: %v", err)
http.Error(w, "Veri okuma hatası: "+err.Error(), http.StatusInternalServerError)
return
}
log.Printf("✅ [ORDERINV] %s / %s / %s -> %d kayıt döndü", code, color, color2, len(list))
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if err := json.NewEncoder(w).Encode(list); err != nil {
log.Printf("❌ [ORDERINV] JSON encode hatası: %v", err)
http.Error(w, "JSON encode başarısız", http.StatusInternalServerError)
}
}

126
svc/routes/orderlist.go Normal file
View File

@@ -0,0 +1,126 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/db"
"bssapp-backend/models"
"bssapp-backend/queries"
"database/sql"
"encoding/json"
"log"
"net/http"
"strings"
)
// ======================================================
// 📌 OrderListRoute — Sipariş Listeleme API (AUTHZ + SAFE)
// ======================================================
func OrderListRoute(mssql *sql.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
// --------------------------------------------------
// 🔍 Query Param (RAW + TRIM)
// --------------------------------------------------
raw := r.URL.Query().Get("search")
search := strings.TrimSpace(raw)
log.Printf(
"📥 /api/orders/list search raw=%q trimmed=%q lenRaw=%d lenTrim=%d",
raw,
search,
len(raw),
len(search),
)
// --------------------------------------------------
// 🗄️ SQL CALL (WITH CONTEXT)
// --------------------------------------------------
rows, err := queries.GetOrderList(
r.Context(),
mssql,
db.PgDB, // ✅ artık var
search,
)
if err != nil {
log.Printf("❌ SQL sorgu hatası: %v", err)
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
return
}
defer rows.Close()
// --------------------------------------------------
// 📦 Sonuç Listesi
// --------------------------------------------------
list := make([]models.OrderList, 0, 100)
count := 0
// ==================================================
// 🧠 SCAN — SQL SELECT ile BİRE BİR (14 kolon)
// ==================================================
for rows.Next() {
var o models.OrderList
err = rows.Scan(
&o.OrderHeaderID, // 1
&o.OrderNumber, // 2
&o.OrderDate, // 3
&o.CurrAccCode, // 4
&o.CurrAccDescription, // 5
&o.MusteriTemsilcisi, // 6
&o.Piyasa, // 7
&o.CreditableConfirmedDate, // 8
&o.DocCurrencyCode, // 9
&o.TotalAmount, // 10
&o.TotalAmountUSD, // 11
&o.IsCreditableConfirmed, // 12
&o.Description, // 13
&o.ExchangeRateUSD, // 14
)
if err != nil {
log.Printf(
"⚠️ SCAN HATASI | OrderHeaderID=%v | err=%v",
o.OrderHeaderID,
err,
)
continue
}
list = append(list, o)
count++
}
if err := rows.Err(); err != nil {
log.Printf("⚠️ rows.Err(): %v", err)
}
// --------------------------------------------------
// 📊 RESULT LOG
// --------------------------------------------------
claims, _ := auth.GetClaimsFromContext(r.Context())
log.Printf(
"✅ Order list DONE | user=%d | search=%q | resultCount=%d",
claims.ID,
search,
count,
)
// --------------------------------------------------
// 📤 JSON OUTPUT
// --------------------------------------------------
if err := json.NewEncoder(w).Encode(list); err != nil {
log.Printf("❌ JSON encode hatası: %v", err)
}
})
}

View File

@@ -0,0 +1,131 @@
package routes
import (
"bssapp-backend/models"
"bssapp-backend/queries"
"database/sql"
"encoding/json"
"fmt"
"net/http"
"strings"
)
// ✅ Model + pb/currency paramına göre fiyat döner (debug loglu)
func GetOrderPriceListB2BHandler(pg *sql.DB, mssql *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
model := r.URL.Query().Get("model")
if model == "" {
model = r.URL.Query().Get("code")
}
cur := r.URL.Query().Get("currency")
if cur == "" {
cur = r.URL.Query().Get("pb")
}
if model == "" || cur == "" {
http.Error(w, "Eksik parametre", 400)
return
}
cur = strings.ToUpper(cur)
fmt.Printf("💰 [MINPRICE] model=%s cur=%s\n", model, cur)
var (
price float64
rate float64 = 1
)
/* ======================
USD / EUR → Direkt
====================== */
if cur == "USD" || cur == "EUR" {
p, err := queries.GetOrderPriceListB2B(pg, model, cur)
if err != nil {
http.Error(w, cur+" fiyat bulunamadı", 404)
return
}
price = p.Price
} else if cur == "TRY" {
/* ======================
TRY → USD * Kur
====================== */
// 1⃣ USD fiyat
p, err := queries.GetOrderPriceListB2B(pg, model, "USD")
if err != nil {
http.Error(w, "USD fiyat bulunamadı", 404)
return
}
// 2⃣ USD→TRY satış
r, err := queries.GetCachedCurrencyV3(mssql, "USD")
if err != nil {
http.Error(w, "USD kuru bulunamadı", 503)
return
}
rate = r.Rate
price = p.Price * rate
} else if cur == "GBP" {
/* ======================
GBP → USD Bridge
====================== */
// 1⃣ USD fiyat
p, err := queries.GetOrderPriceListB2B(pg, model, "USD")
if err != nil {
http.Error(w, "USD fiyat bulunamadı", 404)
return
}
// 2⃣ USD→TRY
usd, err := queries.GetCachedCurrencyV3(mssql, "USD")
if err != nil {
http.Error(w, "USD kuru yok", 503)
return
}
// 3⃣ GBP→TRY
gbp, err := queries.GetCachedCurrencyV3(mssql, "GBP")
if err != nil {
http.Error(w, "GBP kuru yok", 503)
return
}
rate = usd.Rate / gbp.Rate
price = p.Price * rate
} else {
http.Error(w, "Desteklenmeyen para birimi", 400)
return
}
resp := models.OrderPriceListB2B{
ModelCode: model,
CurrencyCode: cur,
Price: price,
RateToTRY: rate,
PriceTRY: price,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
fmt.Printf("✅ [MINPRICE] %s → %.2f %s\n", model, price, cur)
}
}

239
svc/routes/orders.go Normal file
View File

@@ -0,0 +1,239 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/models"
"bssapp-backend/queries"
"bssapp-backend/utils"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/gorilla/mux"
)
// ================================
// POST /api/order/update
// ================================
func UpdateOrderHandler(w http.ResponseWriter, r *http.Request) {
// --------------------------------------------------
// 1⃣ JWT CLAIMS (TEK KAYNAK)
// --------------------------------------------------
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
user := utils.UserFromClaims(claims)
if !ok || claims == nil {
http.Error(w, "Kullanıcı doğrulanamadı", http.StatusUnauthorized)
return
}
user = utils.UserFromClaims(claims)
if user == nil {
http.Error(w, "Kullanıcı doğrulanamadı", http.StatusUnauthorized)
return
}
// --------------------------------------------------
// 2⃣ REQUEST BODY
// --------------------------------------------------
var payload struct {
Header models.OrderHeader `json:"header"`
Lines []models.OrderDetail `json:"lines"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "Geçersiz JSON", http.StatusBadRequest)
return
}
// --------------------------------------------------
// 3⃣ UPDATE
// --------------------------------------------------
results, err := queries.UpdateOrder(
payload.Header,
payload.Lines,
user, // ✅ *models.User
)
if err != nil {
// ✅ VALIDATION ERROR
var vErr *models.ValidationError
if errors.As(err, &vErr) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(vErr)
return
}
// ❌ SYSTEM ERROR
utils.LogError("ORDER_UPDATE", err)
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(map[string]any{
"code": "ORDER_UPDATE_FAILED",
"message": "Sipariş kaydedilirken beklenmeyen bir hata oluştu.",
})
return
}
// --------------------------------------------------
// 4⃣ RESPONSE
// --------------------------------------------------
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"success": true,
"lines": results,
})
}
// -------------------------------------------------------------
// 🟩 CREATE — /api/order/create
// -------------------------------------------------------------
func CreateOrderHandler(pg *sql.DB, mssql *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
// --------------------------------------------------
// JWT CLAIMS
// --------------------------------------------------
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
user := utils.UserFromClaims(claims)
if !ok || claims == nil {
http.Error(w, "Yetkisiz", http.StatusUnauthorized)
return
}
user = utils.UserFromClaims(claims)
if user == nil {
http.Error(w, "Yetkisiz", http.StatusUnauthorized)
return
}
var payload struct {
Header models.OrderHeader `json:"header"`
Lines []models.OrderDetail `json:"lines"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "Geçersiz JSON", http.StatusBadRequest)
return
}
// --------------------------------------------------
// INSERT
// --------------------------------------------------
newID, lineResults, err := queries.InsertOrder(
payload.Header,
payload.Lines,
user, // ✅ *models.User
)
if err != nil {
var vErr *models.ValidationError
if errors.As(err, &vErr) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(vErr)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
orderNo := ""
if payload.Header.OrderNumber.Valid {
orderNo = payload.Header.OrderNumber.String
}
// --------------------------------------------------
// RESPONSE
// --------------------------------------------------
_ = json.NewEncoder(w).Encode(map[string]any{
"status": "success",
"orderID": newID,
"orderNumber": orderNo,
"lineResults": lineResults,
})
}
}
// -------------------------------------------------------------
// 🟨 GET BY ID — /api/order/get/{id}
// -------------------------------------------------------------
func GetOrderByIDHandler(mssql *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
orderID := mux.Vars(r)["id"]
if orderID == "" {
http.Error(w, "Eksik parametre: id", http.StatusBadRequest)
return
}
fmt.Printf("📦 /api/order/get/%s çağrıldı\n", orderID)
header, lines, err := queries.GetOrderByID(orderID)
switch {
case errors.Is(err, sql.ErrNoRows):
http.Error(w, fmt.Sprintf("Sipariş bulunamadı: %s", orderID), http.StatusNotFound)
return
case err != nil:
http.Error(w, fmt.Sprintf("Veritabanı hatası: %v", err), http.StatusInternalServerError)
return
default:
_ = json.NewEncoder(w).Encode(map[string]any{
"header": header,
"lines": lines,
})
}
}
}
// -------------------------------------------------------------
// 🔎 ORDER EXISTS — /api/order/check/{id}
// -------------------------------------------------------------
func OrderExistsHandler(db *sql.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
var count int
err := db.QueryRow(`
SELECT COUNT(*)
FROM trOrderHeader
WHERE OrderHeaderID = @p1
`, id).Scan(&count)
if err != nil {
http.Error(w, "db error", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(map[string]any{
"exists": count > 0,
})
})
}

View File

@@ -0,0 +1,116 @@
package routes
import (
"bssapp-backend/internal/auditlog"
"bssapp-backend/internal/mailer"
"bssapp-backend/internal/security"
"database/sql"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"time"
)
func ForgotPasswordHandler(
db *sql.DB,
mailer *mailer.GraphMailer,
) http.HandlerFunc {
type request struct {
Email string `json:"email"`
}
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
// -------------------------------------------------------
// 1⃣ Request parse (enumeration yok)
// -------------------------------------------------------
var req request
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondOK(w)
return
}
email := strings.TrimSpace(strings.ToLower(req.Email))
if email == "" {
respondOK(w)
return
}
// -------------------------------------------------------
// 2⃣ Aktif kullanıcıyı bul
// -------------------------------------------------------
var userID int64
err := db.QueryRow(`
SELECT id
FROM mk_dfusr
WHERE email = $1
AND is_active = true
`, email).Scan(&userID)
if err != nil {
// ❗ kullanıcı yok → bilgi sızdırma yok
respondOK(w)
return
}
// -------------------------------------------------------
// 3⃣ Reset token üret
// -------------------------------------------------------
plain, hash, err := security.GenerateResetToken()
if err != nil {
respondOK(w)
return
}
expires := time.Now().Add(30 * time.Minute)
// -------------------------------------------------------
// 4⃣ DBye SADECE HASH kaydet
// -------------------------------------------------------
_, _ = db.Exec(`
INSERT INTO dfusr_password_reset (
dfusr_id,
token,
expires_at
)
VALUES ($1, $2, $3)
`, userID, hash, expires)
// -------------------------------------------------------
// 5⃣ Reset URL (PLAIN token)
// -------------------------------------------------------
resetURL := fmt.Sprintf(
"%s/password-reset/%s",
os.Getenv("FRONTEND_URL"),
plain,
)
// -------------------------------------------------------
// 6⃣ Mail gönder (fail olsa bile enumeration yok)
// -------------------------------------------------------
_ = mailer.SendPasswordResetMail(email, resetURL)
// -------------------------------------------------------
// 7⃣ AUDIT LOG
// -------------------------------------------------------
auditlog.Write(auditlog.ActivityLog{
ActionType: "PASSWORD_FORGOT_REQUEST",
ActionCategory: "security",
ActionTarget: email,
IsSuccess: true,
})
respondOK(w)
}
}
func respondOK(w http.ResponseWriter) {
_ = json.NewEncoder(w).Encode(map[string]bool{
"success": true,
})
}

View File

@@ -0,0 +1,126 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/internal/auditlog"
"bssapp-backend/internal/security"
"database/sql"
"encoding/json"
"net/http"
"golang.org/x/crypto/bcrypt"
)
func ChangeOwnPasswordHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
// --------------------------------------------------
// 1⃣ JWT CLAIMS
// --------------------------------------------------
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
// --------------------------------------------------
// 2⃣ PAYLOAD
// --------------------------------------------------
var req struct {
CurrentPassword string `json:"current_password"`
NewPassword string `json:"new_password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid payload", http.StatusBadRequest)
return
}
if req.CurrentPassword == "" || req.NewPassword == "" {
http.Error(w, "current_password and new_password required", http.StatusBadRequest)
return
}
// --------------------------------------------------
// 3⃣ PASSWORD POLICY
// --------------------------------------------------
if err := security.ValidatePassword(req.NewPassword); err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
// --------------------------------------------------
// 4⃣ MEVCUT HASH ÇEK
// --------------------------------------------------
var currentHash string
err := db.QueryRow(`
SELECT password_hash
FROM mk_dfusr
WHERE id = $1
AND is_active = true
`, claims.ID).Scan(&currentHash)
if err != nil {
http.Error(w, "user not found", http.StatusUnauthorized)
return
}
// --------------------------------------------------
// 5⃣ CURRENT PASSWORD CHECK
// --------------------------------------------------
if bcrypt.CompareHashAndPassword(
[]byte(currentHash),
[]byte(req.CurrentPassword),
) != nil {
http.Error(w, "current password incorrect", http.StatusUnauthorized)
return
}
// --------------------------------------------------
// 6⃣ NEW HASH
// --------------------------------------------------
newHash, err := bcrypt.GenerateFromPassword(
[]byte(req.NewPassword),
bcrypt.DefaultCost,
)
if err != nil {
http.Error(w, "hash error", http.StatusInternalServerError)
return
}
// --------------------------------------------------
// 7⃣ UPDATE (⚠️ force_password_change DEĞİŞMEZ)
// --------------------------------------------------
_, err = db.Exec(`
UPDATE mk_dfusr
SET
password_hash = $1,
password_updated_at = now(),
updated_at = now()
WHERE id = $2
`, string(newHash), claims.ID)
if err != nil {
http.Error(w, "password update failed", http.StatusInternalServerError)
return
}
// --------------------------------------------------
// 8⃣ AUDIT
// --------------------------------------------------
auditlog.Write(auditlog.ActivityLog{
ActionType: "PASSWORD_CHANGED",
ActionCategory: "security",
ActionTarget: claims.Username,
IsSuccess: true,
})
// --------------------------------------------------
// 9⃣ RESPONSE
// --------------------------------------------------
_ = json.NewEncoder(w).Encode(map[string]any{
"success": true,
})
}
}

View File

@@ -0,0 +1,152 @@
// routes/password_reset_complete.go
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/internal/auditlog"
"bssapp-backend/internal/security"
"bssapp-backend/repository"
"database/sql"
"encoding/json"
"net/http"
"time"
"golang.org/x/crypto/bcrypt"
)
func CompletePasswordResetHandler(db *sql.DB) http.HandlerFunc {
type request struct {
Token string `json:"token"`
Password string `json:"password"`
}
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
var req request
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Token == "" || req.Password == "" {
http.Error(w, "invalid payload", http.StatusBadRequest)
return
}
if err := security.ValidatePassword(req.Password); err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
tokenHash := security.HashToken(req.Token)
// 1) token doğrula
var userID int64
var expiresAt time.Time
err := db.QueryRow(`
SELECT mk_dfusr_id, expires_at
FROM mk_dfusr_password_reset
WHERE token = $1
AND used_at IS NULL
`, tokenHash).Scan(&userID, &expiresAt)
if err != nil || time.Now().After(expiresAt) {
http.Error(w, "invalid or expired token", http.StatusUnprocessableEntity)
return
}
// 2) yeni hash
passHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
http.Error(w, "hash error", http.StatusInternalServerError)
return
}
// 3) tx: şifre + used_at
tx, err := db.Begin()
if err != nil {
http.Error(w, "tx error", http.StatusInternalServerError)
return
}
defer tx.Rollback()
// mk_dfusr güncelle
if _, err := tx.Exec(`
UPDATE mk_dfusr
SET password_hash = $1,
force_password_change = false,
password_updated_at = now(),
updated_at = now()
WHERE id = $2
`, string(passHash), userID); err != nil {
http.Error(w, "update failed", http.StatusInternalServerError)
return
}
// token tüket
if _, err := tx.Exec(`
UPDATE mk_dfusr_password_reset
SET used_at = now()
WHERE token = $1
`, tokenHash); err != nil {
http.Error(w, "token update failed", http.StatusInternalServerError)
return
}
if err := tx.Commit(); err != nil {
http.Error(w, "commit failed", http.StatusInternalServerError)
return
}
// 4) refresh revoke
_ = repository.NewRefreshTokenRepository(db).RevokeAllForUser(userID)
// 5) user + perms + jwt
mkRepo := repository.NewMkUserRepository(db)
u, err := mkRepo.GetByID(userID)
if err != nil {
http.Error(w, "user fetch failed", http.StatusInternalServerError)
return
}
permRepo := repository.NewPermissionRepository(db)
perms, _ := permRepo.GetPermissionsByRoleID(u.RoleID)
// ✅ CLAIMS BUILD
claims := auth.BuildClaimsFromUser(u, 15*time.Minute)
// ✅ TOKEN
jwtStr, err := auth.GenerateToken(
claims,
u.Username,
false,
)
if err != nil {
http.Error(w, "token generation failed", http.StatusInternalServerError)
return
}
auditlog.Write(auditlog.ActivityLog{
ActionType: "PASSWORD_RESET_COMPLETED",
ActionCategory: "security",
ActionTarget: u.Username,
IsSuccess: true,
})
_ = json.NewEncoder(w).Encode(map[string]any{
"success": true,
"token": jwtStr,
"user": map[string]any{
"id": u.ID,
"username": u.Username,
"email": u.Email,
"is_active": u.IsActive,
"role_id": u.RoleID,
"role_code": u.RoleCode,
"force_password_change": false,
"v3_username": u.V3Username,
"v3_usergroup": u.V3UserGroup,
},
"permissions": perms,
})
}
}

View File

@@ -0,0 +1,63 @@
package routes
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"net/http"
"time"
"github.com/gorilla/mux"
)
// GET /api/password/reset/validate/{token}
func ValidatePasswordResetTokenHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := mux.Vars(r)["token"]
if token == "" {
http.NotFound(w, r)
return
}
// 🔐 plain token -> hash
h := sha256.Sum256([]byte(token))
tokenHash := hex.EncodeToString(h[:])
var (
userID int64
expiresAt time.Time
usedAt sql.NullTime
)
err := db.QueryRow(`
SELECT user_id, expires_at, used_at
FROM password_reset_tokens
WHERE token_hash = $1
LIMIT 1
`, tokenHash).Scan(&userID, &expiresAt, &usedAt)
if err != nil {
// ❗ bilgi sızdırma yok
http.NotFound(w, r)
return
}
// ⏰ Süre kontrolü
if time.Now().After(expiresAt) {
http.NotFound(w, r)
return
}
// 🔁 Tek kullanımlık
if usedAt.Valid {
http.Error(w, "token already used", http.StatusGone)
return
}
// ✅ TOKEN GEÇERLİ
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"valid":true}`))
}
}

View File

@@ -0,0 +1,63 @@
package routes
import (
"bssapp-backend/auth"
"database/sql"
"encoding/json"
"net/http"
)
func DebugPermissionV2(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// sadece auth kontrolü
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", 401)
return
}
module := r.URL.Query().Get("module")
action := r.URL.Query().Get("action")
if module == "" || action == "" {
http.Error(w, "module & action required", 400)
return
}
rows, err := db.Query(`
SELECT
r.id,
r.code,
rp.allowed
FROM dfrole r
LEFT JOIN mk_sys_role_permissions rp
ON rp.role_id = r.id
AND rp.module_code = $1
AND rp.action = $2
ORDER BY r.id
`, module, action)
if err != nil {
http.Error(w, "db error", 500)
return
}
defer rows.Close()
type Row struct {
RoleID int `json:"role_id"`
Code string `json:"code"`
Allowed bool `json:"allowed"`
}
var list []Row
for rows.Next() {
var r Row
_ = rows.Scan(&r.RoleID, &r.Code, &r.Allowed)
list = append(list, r)
}
json.NewEncoder(w).Encode(list)
}
}

View File

@@ -0,0 +1,203 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/internal/auditlog"
"bssapp-backend/permissions"
"database/sql"
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/gorilla/mux"
)
func GetRolePermissionMatrix(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", 401)
return
}
roleID, _ := strconv.Atoi(mux.Vars(r)["id"])
rows, err := db.Query(`
SELECT
module_code,
action,
allowed
FROM mk_sys_role_permissions
WHERE role_id=$1
`, roleID)
if err != nil {
http.Error(w, "db error", 500)
return
}
defer rows.Close()
var list []permissions.PermissionMatrixRow
for rows.Next() {
var row permissions.PermissionMatrixRow
if err := rows.Scan(
&row.Module,
&row.Action,
&row.Allowed,
); err != nil {
http.Error(w, "scan error", 500)
return
}
row.Source = "role"
list = append(list, row)
}
json.NewEncoder(w).Encode(list)
}
}
func SaveRolePermissionMatrix(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", 401)
return
}
roleID, _ := strconv.Atoi(mux.Vars(r)["id"])
var list []permissions.PermissionMatrixRow
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
http.Error(w, "bad payload", 400)
return
}
repo := permissions.NewPermissionRepository(db)
// ================= OLD =================
oldRows, _ := repo.GetPermissionMatrixForRoles([]int{roleID})
oldMap := map[string]bool{}
for _, p := range oldRows {
key := p.Module + ":" + p.Action
oldMap[key] = p.Allowed
}
// ================= DIFF =================
var changes []map[string]any
for _, p := range list {
key := p.Module + ":" + p.Action
oldVal, ok := oldMap[key]
if !ok {
oldVal = false
}
if oldVal != p.Allowed {
changes = append(changes, map[string]any{
"module": p.Module,
"action": p.Action,
"before": oldVal,
"after": p.Allowed,
})
}
}
// ================= SAVE =================
tx, err := db.Begin()
if err != nil {
http.Error(w, "tx error", 500)
return
}
defer tx.Rollback()
stmt, err := tx.Prepare(`
INSERT INTO mk_sys_role_permissions
(role_id,module_code,action,allowed)
VALUES ($1,$2,$3,$4)
ON CONFLICT
(role_id,module_code,action)
DO UPDATE SET allowed=EXCLUDED.allowed
`)
if err != nil {
http.Error(w, "prepare error", 500)
return
}
defer stmt.Close()
for _, p := range list {
if _, err := stmt.Exec(
roleID,
p.Module,
p.Action,
p.Allowed,
); err != nil {
http.Error(w, "exec error", 500)
return
}
}
if err := tx.Commit(); err != nil {
http.Error(w, "commit error", 500)
return
}
// ================= AUDIT =================
if len(changes) > 0 {
var roleCode string
_ = db.QueryRow(`
SELECT code FROM dfrole WHERE id=$1
`, roleID).Scan(&roleCode)
auditlog.Enqueue(r.Context(), auditlog.ActivityLog{
ActionType: "role_permission_change",
ActionCategory: "role_permission",
ActionTarget: fmt.Sprintf("/api/roles/%d/permissions", roleID),
Description: "role permission matrix updated",
Username: claims.Username,
RoleCode: claims.RoleCode,
DfUsrID: int64(claims.ID),
ChangeBefore: map[string]any{
"permissions": oldRows,
},
ChangeAfter: map[string]any{
"changes": changes,
},
IsSuccess: true,
})
}
json.NewEncoder(w).Encode(map[string]bool{
"success": true,
})
}
}

114
svc/routes/permissions.go Normal file
View File

@@ -0,0 +1,114 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/permissions"
"database/sql"
"encoding/json"
"net/http"
)
/* =====================================================
HANDLER
===================================================== */
type PermissionHandler struct {
DB *sql.DB
Repo *permissions.PermissionRepository
}
func NewPermissionHandler(db *sql.DB) *PermissionHandler {
return &PermissionHandler{
DB: db,
Repo: permissions.NewPermissionRepository(db),
}
}
/* =====================================================
POST /api/permissions/matrix
===================================================== */
func (h *PermissionHandler) UpdatePermissionMatrix(
w http.ResponseWriter,
r *http.Request,
) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", 401)
return
}
var req []permissions.PermissionUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid payload", 400)
return
}
if len(req) == 0 {
w.WriteHeader(http.StatusOK)
return
}
err := h.Repo.UpdatePermissions(req)
if err != nil {
http.Error(w, "db error", 500)
return
}
json.NewEncoder(w).Encode(map[string]any{
"success": true,
})
}
func GetMyPermissionMatrix(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", 401)
return
}
var roleID int
err := db.QueryRow(`
SELECT id FROM dfrole WHERE LOWER(code)=LOWER($1)
`, claims.RoleCode).Scan(&roleID)
if err != nil {
http.Error(w, "role resolve error", 500)
return
}
repo := permissions.NewPermissionRepository(db)
raw, err := repo.GetPermissionMatrixForRoles([]int{roleID})
if err != nil {
http.Error(w, "db error", 500)
return
}
// 🔥 FRONTEND FORMAT
type Row struct {
Module string `json:"module"`
Action string `json:"action"`
Allowed bool `json:"allowed"`
}
list := make([]Row, 0, len(raw))
for _, p := range raw {
list = append(list, Row{
Module: p.ModuleCode, // 👈 burası önemli
Action: p.Action,
Allowed: p.Allowed,
})
}
_ = json.NewEncoder(w).Encode(list)
}
}

27
svc/routes/product.go Normal file
View File

@@ -0,0 +1,27 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/queries"
"encoding/json"
"net/http"
)
// ✅ GET /api/products
func GetProductListHandler(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
products, err := queries.GetProductList()
if err != nil {
http.Error(w, "Ürün listesi alınamadı: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(products)
}

View File

@@ -0,0 +1,48 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/db"
"bssapp-backend/models"
"bssapp-backend/queries"
"encoding/json"
"log"
"net/http"
)
func GetProductColorsHandler(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
code := r.URL.Query().Get("code")
if code == "" {
http.Error(w, "Eksik parametre: code gerekli", http.StatusBadRequest)
return
}
rows, err := db.MssqlDB.Query(queries.GetProductColors, code)
if err != nil {
http.Error(w, "Renk listesi alınamadı: "+err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var list []models.ProductColor
for rows.Next() {
var c models.ProductColor
if err := rows.Scan(&c.ProductCode, &c.ColorCode, &c.ColorDescription); err != nil {
log.Println("Satır okunamadı:", err)
continue
}
list = append(list, c)
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(list)
log.Printf("✅ [1.COLOR] %s -> %d kayıt", code, len(list))
}

View File

@@ -0,0 +1,73 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/db"
"bssapp-backend/models"
"bssapp-backend/queries"
"encoding/json"
"log"
"net/http"
)
// ✅ GET /api/product-colorsize?code=S001-DCY18144&color=001&color2=017
func GetProductColorSizesHandler(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
code := r.URL.Query().Get("code")
color := r.URL.Query().Get("color")
color2 := r.URL.Query().Get("color2")
// 🔹 Model kodu zorunlu, renk opsiyonel
if code == "" {
http.Error(w, "Eksik parametre: code gerekli", http.StatusBadRequest)
return
}
// 🔧 Eksik parametreleri normalize et
if color == "" {
color = " "
}
if color2 == "" {
color2 = " "
}
log.Printf("🎨 [COLORSIZE] İstek alındı -> code=%q | color=%q | color2=%q", code, color, color2)
// ✅ MSSQLe sıralı 3 parametre gönder (adlı değil!)
rows, err := db.MssqlDB.Query(queries.GetProductColorSizes, code, color, color2)
if err != nil {
log.Printf("❌ [COLORSIZE] DB sorgu hatası: %v", err)
http.Error(w, "SQL hatası: "+err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var list []models.ProductColorSize
for rows.Next() {
var s models.ProductColorSize
if err := rows.Scan(&s.ProductCode, &s.ColorCode, &s.ItemDim1Code, &s.ItemDim2Code); err != nil {
log.Printf("⚠️ [COLORSIZE] Satır okunamadı: %v", err)
continue
}
list = append(list, s)
}
if err := rows.Err(); err != nil {
log.Printf("⚠️ [COLORSIZE] Row iteration hatası: %v", err)
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if err := json.NewEncoder(w).Encode(list); err != nil {
log.Printf("❌ [COLORSIZE] JSON encode hatası: %v", err)
http.Error(w, "JSON encode başarısız", http.StatusInternalServerError)
return
}
log.Printf("✅ [COLORSIZE] %s / %s / %s -> %d kayıt döndü", code, color, color2, len(list))
}

View File

@@ -0,0 +1,65 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/db"
"bssapp-backend/models"
"bssapp-backend/queries"
"encoding/json"
"log"
"net/http"
)
func GetProductDetailHandler(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
code := r.URL.Query().Get("code")
if code == "" {
http.Error(w, "Eksik parametre: code", http.StatusBadRequest)
return
}
row := db.MssqlDB.QueryRow(queries.GetProductDetail, code)
var p models.ProductDetail
err := row.Scan(
&p.ProductCode,
new(string), // BOS7
new(string), // ProductDescription
new(string), // ProductAtt42
&p.UrunIlkGrubu,
new(string), // ProductAtt01
&p.UrunAnaGrubu,
new(string), // ProductAtt02
&p.UrunAltGrubu,
new(string), // ProductAtt41
&p.UrunIcerik,
new(string), // ProductAtt10
new(string), // Marka
new(string), // ProductAtt11
&p.Drop,
new(string), // ProductAtt29
new(string), // Karisim
new(string), // ProductAtt45
&p.AskiliYan,
new(string), // ProductAtt44
&p.Kategori,
new(string), // ProductAtt38
&p.Fit1,
new(string), // ProductAtt39
&p.Fit2,
)
if err != nil {
log.Println("❌ Ürün detayları alınamadı:", err)
http.Error(w, "Veri okuma hatası: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(p)
}

View File

@@ -0,0 +1,67 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/db"
"bssapp-backend/models"
"bssapp-backend/queries"
"database/sql"
"encoding/json"
"log"
"net/http"
)
// ✅ GET /api/product-secondcolor?code=S001-DCY18144&color=001
func GetProductSecondColorsHandler(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
code := r.URL.Query().Get("code")
color := r.URL.Query().Get("color")
if code == "" || color == "" {
http.Error(w, "Eksik parametre: code ve color gerekli", http.StatusBadRequest)
return
}
log.Printf("🎨 [2.COLOR] İstek alındı -> code=%s color=%s", code, color)
rows, err := db.MssqlDB.Query(
queries.GetProductSecondColors,
sql.Named("ProductCode", code),
sql.Named("ColorCode", color),
)
if err != nil {
log.Println("❌ DB sorgu hatası:", err)
http.Error(w, "2. renk listesi alınamadı: "+err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var list []models.ProductSecondColor
for rows.Next() {
var c models.ProductSecondColor
if err := rows.Scan(&c.ProductCode, &c.ColorCode, &c.ItemDim2Code); err != nil {
log.Println("⚠️ Satır okunamadı:", err)
continue
}
list = append(list, c)
}
if err := rows.Err(); err != nil {
log.Println("⚠️ Row iteration hatası:", err)
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if err := json.NewEncoder(w).Encode(list); err != nil {
log.Println("❌ JSON encode hatası:", err)
http.Error(w, "JSON encode başarısız", http.StatusInternalServerError)
return
}
log.Printf("✅ [2.COLOR] %s / %s -> %d kayıt döndü", code, color, len(list))
}

View File

@@ -0,0 +1,440 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/internal/auditlog"
"bssapp-backend/permissions"
"bssapp-backend/queries"
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"github.com/gorilla/mux"
)
type IdTitleOption struct {
ID string `json:"id"`
Title string `json:"title"`
}
type Row struct {
Route string `json:"route"`
CanAccess bool `json:"can_access"`
}
type RoleDepartmentPermissionHandler struct {
DB *sql.DB
Repo *permissions.RoleDepartmentPermissionRepo
}
func NewRoleDepartmentPermissionHandler(db *sql.DB) *RoleDepartmentPermissionHandler {
return &RoleDepartmentPermissionHandler{
DB: db, // ✅ EKLENDİ
Repo: permissions.NewRoleDepartmentPermissionRepo(db),
}
}
/* ======================================================
GET
====================================================== */
func (h *RoleDepartmentPermissionHandler) Get(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
vars := mux.Vars(r)
roleID, err := strconv.Atoi(vars["roleId"])
if err != nil || roleID <= 0 {
http.Error(w, "invalid roleId", http.StatusBadRequest)
return
}
deptCode := vars["deptCode"]
if deptCode == "" {
http.Error(w, "invalid deptCode", http.StatusBadRequest)
return
}
list, err := h.Repo.Get(roleID, deptCode)
if err != nil {
http.Error(w, "db error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(list)
}
/* ======================================================
POST
====================================================== */
func (h *RoleDepartmentPermissionHandler) Save(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
vars := mux.Vars(r)
roleID, err := strconv.Atoi(vars["roleId"])
if err != nil || roleID <= 0 {
http.Error(w, "invalid roleId", http.StatusBadRequest)
return
}
deptCode := vars["deptCode"]
if deptCode == "" {
http.Error(w, "invalid deptCode", http.StatusBadRequest)
return
}
var list []permissions.RoleDepartmentPermission
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
http.Error(w, "bad payload", http.StatusBadRequest)
return
}
// ================= OLD =================
oldRows, err := h.Repo.Get(roleID, deptCode)
if err != nil {
log.Println("OLD PERM LOAD ERROR:", err)
}
oldMap := map[string]bool{}
for _, p := range oldRows {
key := p.Module + ":" + p.Action
oldMap[key] = p.Allowed
}
// ================= DIFF =================
var changes []map[string]any
for _, p := range list {
key := p.Module + ":" + p.Action
oldVal := oldMap[key]
if oldVal != p.Allowed {
changes = append(changes, map[string]any{
"module": p.Module,
"action": p.Action,
"before": oldVal,
"after": p.Allowed,
})
}
}
// ================= SAVE =================
if err := h.Repo.Save(roleID, deptCode, list); err != nil {
http.Error(w, "save error", http.StatusInternalServerError)
return
}
// ================= AUDIT =================
if len(changes) > 0 {
auditlog.Enqueue(r.Context(), auditlog.ActivityLog{
ActionType: "role_department_permission_change",
ActionCategory: "role_permission",
ActionTarget: fmt.Sprintf(
"/api/roles/%d/departments/%s/permissions",
roleID,
deptCode,
),
Description: "role+department permissions updated",
Username: claims.Username,
RoleCode: claims.RoleCode,
DfUsrID: int64(claims.ID),
ChangeBefore: map[string]any{
"permissions": oldRows,
},
ChangeAfter: map[string]any{
"changes": changes,
},
IsSuccess: true,
})
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
func GetModuleLookupRoute(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query(queries.GetModuleLookup)
if err != nil {
http.Error(w, "db error", 500)
return
}
defer rows.Close()
type Row struct {
Value string `json:"value"`
Label string `json:"label"`
}
var list []Row
for rows.Next() {
var r Row
if err := rows.Scan(
&r.Value,
&r.Label,
); err != nil {
http.Error(w, "scan error", 500)
return
}
list = append(list, r)
}
json.NewEncoder(w).Encode(list)
}
}
func GetRolesForPermissionSelectRoute(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
rows, err := db.Query(queries.GetRolesForPermissionSelect)
if err != nil {
http.Error(w, "roles select error", 500)
return
}
defer rows.Close()
list := make([]IdTitleOption, 0)
for rows.Next() {
var o IdTitleOption
if err := rows.Scan(&o.ID, &o.Title); err == nil {
list = append(list, o)
}
}
_ = json.NewEncoder(w).Encode(list)
}
}
func GetDepartmentsForPermissionSelectRoute(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
rows, err := db.Query(queries.GetDepartmentsForPermissionSelect)
if err != nil {
http.Error(w, "departments select error", 500)
return
}
defer rows.Close()
list := make([]IdTitleOption, 0)
for rows.Next() {
var o IdTitleOption
if err := rows.Scan(&o.ID, &o.Title); err == nil {
list = append(list, o)
}
}
_ = json.NewEncoder(w).Encode(list)
}
}
func GetUsersForPermissionSelectRoute(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
rows, err := db.Query(queries.GetUserLookupForPermission)
if err != nil {
http.Error(w, "users lookup error", http.StatusInternalServerError)
return
}
defer rows.Close()
list := make([]IdTitleOption, 0, 128)
for rows.Next() {
var o IdTitleOption
if err := rows.Scan(&o.ID, &o.Title); err == nil {
list = append(list, o)
}
}
_ = json.NewEncoder(w).Encode(list)
}
}
func (h *PermissionHandler) GetUserOverrides(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
userID, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
if err != nil || userID <= 0 {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
list, err := h.Repo.GetUserOverridesByUserID(userID)
if err != nil {
log.Println("❌ USER OVERRIDE LOAD ERROR:", err)
http.Error(w, "db error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(list)
}
func GetUserRoutePermissionsHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", 401)
return
}
repo := permissions.NewPermissionRepository(db)
// JWTden departmanlar
depts := claims.DepartmentCodes
rows, err := db.Query(`
SELECT DISTINCT
module_code,
action,
path
FROM mk_sys_routes
`)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer rows.Close()
type Row struct {
Route string `json:"route"`
CanAccess bool `json:"can_access"`
}
list := make([]Row, 0, 64)
for rows.Next() {
var module, action, path string
if err := rows.Scan(
&module,
&action,
&path,
); err != nil {
continue
}
allowed, err := repo.ResolvePermissionChain(
int64(claims.ID),
int64(claims.RoleID),
depts,
module,
action,
)
if err != nil {
log.Println("PERM RESOLVE ERROR:", err)
continue
}
list = append(list, Row{
Route: path,
CanAccess: allowed,
})
}
_ = json.NewEncoder(w).Encode(list)
}
}
func GetMyEffectivePermissions(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", 401)
return
}
repo := permissions.NewPermissionRepository(db)
// ✅ JWT'DEN DEPARTMENTS
depts := claims.DepartmentCodes
log.Printf("🧪 EFFECTIVE PERM | user=%d role=%d depts=%v",
claims.ID,
claims.RoleID,
depts,
)
// all system perms
all, err := db.Query(`
SELECT DISTINCT module_code, action
FROM mk_sys_routes
`)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer all.Close()
type Row struct {
Module string `json:"module"`
Action string `json:"action"`
Allowed bool `json:"allowed"`
}
list := make([]Row, 0, 128)
for all.Next() {
var m, a string
if err := all.Scan(&m, &a); err != nil {
continue
}
allowed, err := repo.ResolvePermissionChain(
int64(claims.ID),
int64(claims.RoleID),
depts,
m,
a,
)
if err != nil {
continue
}
list = append(list, Row{
Module: m,
Action: a,
Allowed: allowed,
})
}
_ = json.NewEncoder(w).Encode(list)
}
}

View File

@@ -0,0 +1,38 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/queries"
"encoding/json"
"net/http"
"github.com/gorilla/mux"
)
// GET /api/statements/{accountCode}/details?startdate=...&enddate=...&parislemler=1&parislemler=2
func GetStatementDetailsHandler(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
vars := mux.Vars(r)
accountCode := vars["accountCode"]
startDate := r.URL.Query().Get("startdate")
endDate := r.URL.Query().Get("enddate")
parislemler := r.URL.Query()["parislemler"]
details, err := queries.GetStatementDetails(accountCode, startDate, endDate, parislemler)
if err != nil {
http.Error(w, "Error fetching statement details: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if err := json.NewEncoder(w).Encode(details); err != nil {
http.Error(w, "Error encoding response: "+err.Error(), http.StatusInternalServerError)
}
}

View File

@@ -0,0 +1,38 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/models"
"bssapp-backend/queries"
"encoding/json"
"net/http"
)
// GET /api/statements
func GetStatementHeadersHandler(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
params := models.StatementParams{
StartDate: r.URL.Query().Get("startdate"),
EndDate: r.URL.Query().Get("enddate"),
AccountCode: r.URL.Query().Get("accountcode"),
LangCode: r.URL.Query().Get("langcode"),
Parislemler: r.URL.Query()["parislemler"],
}
statements, err := queries.GetStatements(params)
if err != nil {
http.Error(w, "Error fetching statements: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if err := json.NewEncoder(w).Encode(statements); err != nil {
http.Error(w, "Error encoding response: "+err.Error(), http.StatusInternalServerError)
}
}

View File

@@ -0,0 +1,395 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/models"
"bssapp-backend/queries"
"bytes"
"database/sql"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/jung-kurt/gofpdf"
)
/* ============================ SABİTLER ============================ */
// A4 Landscape (mm)
const (
hPageWidth = 297.0
hPageHeight = 210.0
hMarginL = 8.0
hMarginT = 8.0
hMarginR = 8.0
hMarginB = 15.0
hLineHMain = 5.2
hCellPadX = 2.0
hHeaderRowH = 8.4
hGroupBarH = 9.0
hGridLineWidth = 0.3
hLogoW = 42.0
)
// Kolonlar
var hMainCols = []string{
"Belge No", "Tarih", "Vade", "İşlem",
"Açıklama", "Para", "Borç", "Alacak", "Bakiye",
}
var hMainWbase = []float64{
34, 30, 28, 24, 60, 20, 36, 36, 36,
}
// Font dosyaları
const (
hFontFamilyReg = "dejavu"
hFontFamilyBold = "dejavu-b"
hFontPathReg = "fonts/DejaVuSans.ttf"
hFontPathBold = "fonts/DejaVuSans-Bold.ttf"
)
// Renkler
var (
hColorPrimary = [3]int{149, 113, 22} // #957116
hColorSecondary = [3]int{218, 193, 151} // #dac197 (Baggi secondary)
)
/* ============================ FONT / FORMAT ============================ */
func hEnsureFonts(pdf *gofpdf.Fpdf) {
if _, err := os.Stat(hFontPathReg); err == nil {
pdf.AddUTF8Font(hFontFamilyReg, "", hFontPathReg)
}
if _, err := os.Stat(hFontPathBold); err == nil {
pdf.AddUTF8Font(hFontFamilyBold, "", hFontPathBold)
}
}
func hNormalizeWidths(base []float64, targetTotal float64) []float64 {
sum := 0.0
for _, v := range base {
sum += v
}
scale := targetTotal / sum
out := make([]float64, len(base))
for i, v := range base {
out[i] = v * scale
}
return out
}
func hFormatCurrencyTR(val float64) string {
s := fmt.Sprintf("%.2f", val)
parts := strings.Split(s, ".")
intPart, decPart := parts[0], parts[1]
var out []string
for len(intPart) > 3 {
out = append([]string{intPart[len(intPart)-3:]}, out...)
intPart = intPart[:len(intPart)-3]
}
if intPart != "" {
out = append([]string{intPart}, out...)
}
return strings.Join(out, ".") + "," + decPart
}
func hNeedNewPage(pdf *gofpdf.Fpdf, needH float64) bool {
return pdf.GetY()+needH+hMarginB > hPageHeight
}
/* ============================ YARDIMCI FONKSİYONLAR ============================ */
func hSplitLinesSafe(pdf *gofpdf.Fpdf, text string, width float64) [][]byte {
if width <= 0 {
width = 1
}
return pdf.SplitLines([]byte(text), width)
}
func hDrawWrapText(pdf *gofpdf.Fpdf, text string, x, y, width, lineH float64) {
if text == "" {
return
}
lines := hSplitLinesSafe(pdf, text, width)
cy := y
for _, ln := range lines {
pdf.SetXY(x, cy)
pdf.CellFormat(width, lineH, string(ln), "", 0, "L", false, 0, "")
cy += lineH
}
}
func hCalcRowHeightForText(pdf *gofpdf.Fpdf, text string, colWidth, lineHeight, padX float64) float64 {
if text == "" {
return lineHeight
}
lines := hSplitLinesSafe(pdf, text, colWidth-(2*padX))
h := float64(len(lines)) * lineHeight
if h < lineHeight {
h = lineHeight
}
return h
}
/* ============================ HEADER ============================ */
func hDrawPageHeader(pdf *gofpdf.Fpdf, cariKod, cariIsim, start, end string) float64 {
logoPath, _ := filepath.Abs("./public/Baggi-Tekstil-A.s-Logolu.jpeg")
// Logo
pdf.ImageOptions(logoPath, hMarginL, 2, hLogoW, 0, false, gofpdf.ImageOptions{}, 0, "")
// Başlıklar
pdf.SetFont(hFontFamilyBold, "", 16)
pdf.SetTextColor(hColorPrimary[0], hColorPrimary[1], hColorPrimary[2])
pdf.SetXY(hMarginL+hLogoW+8, hMarginT+2)
pdf.CellFormat(120, 7, "Baggi Software System", "", 0, "L", false, 0, "")
pdf.SetFont(hFontFamilyBold, "", 12)
pdf.SetXY(hMarginL+hLogoW+8, hMarginT+10)
pdf.CellFormat(120, 6, "Cari Hesap Raporu", "", 0, "L", false, 0, "")
// Bugünün tarihi (sağ üst)
today := time.Now().Format("02.01.2006")
pdf.SetFont(hFontFamilyReg, "", 9)
pdf.SetXY(hPageWidth-hMarginR-40, hMarginT+3)
pdf.CellFormat(40, 6, "Tarih: "+today, "", 0, "R", false, 0, "")
// Cari & Tarih kutuları (daha yukarı taşındı)
boxY := hMarginT + hLogoW - 6
pdf.SetFont(hFontFamilyBold, "", 10)
pdf.Rect(hMarginL, boxY, 140, 11, "")
pdf.SetXY(hMarginL+2, boxY+3)
pdf.CellFormat(136, 5, fmt.Sprintf("Cari: %s — %s", cariKod, cariIsim), "", 0, "L", false, 0, "")
pdf.Rect(hPageWidth-hMarginR-140, boxY, 140, 11, "")
pdf.SetXY(hPageWidth-hMarginR-138, boxY+3)
pdf.CellFormat(136, 5, fmt.Sprintf("Tarih Aralığı: %s → %s", start, end), "", 0, "R", false, 0, "")
// Alt çizgi
y := boxY + 13
pdf.SetDrawColor(hColorPrimary[0], hColorPrimary[1], hColorPrimary[2])
pdf.Line(hMarginL, y, hPageWidth-hMarginR, y)
pdf.SetDrawColor(200, 200, 200)
return y + 4
}
/* ============================ TABLO ============================ */
func hDrawGroupBar(pdf *gofpdf.Fpdf, currency string, sonBakiye float64) {
x := hMarginL
y := pdf.GetY()
w := hPageWidth - hMarginL - hMarginR
h := hGroupBarH
pdf.SetDrawColor(hColorPrimary[0], hColorPrimary[1], hColorPrimary[2])
pdf.SetLineWidth(0.6)
pdf.Rect(x, y, w, h, "")
pdf.SetFont(hFontFamilyBold, "", 11.3)
pdf.SetTextColor(hColorPrimary[0], hColorPrimary[1], hColorPrimary[2])
pdf.SetXY(x+hCellPadX+1.0, y+(h-5.0)/2)
pdf.CellFormat(w*0.6, 5.0, currency, "", 0, "L", false, 0, "")
txt := "Son Bakiye = " + hFormatCurrencyTR(sonBakiye)
pdf.SetXY(x+w*0.4, y+(h-5.0)/2)
pdf.CellFormat(w*0.6-2.0, 5.0, txt, "", 0, "R", false, 0, "")
pdf.SetLineWidth(hGridLineWidth)
pdf.SetDrawColor(200, 200, 200)
pdf.SetTextColor(0, 0, 0)
pdf.Ln(h)
}
func hDrawMainHeaderRow(pdf *gofpdf.Fpdf, cols []string, widths []float64) {
pdf.SetFont(hFontFamilyBold, "", 10.2)
pdf.SetTextColor(255, 255, 255)
pdf.SetFillColor(hColorPrimary[0], hColorPrimary[1], hColorPrimary[2])
pdf.SetDrawColor(120, 90, 20)
x := hMarginL
y := pdf.GetY()
for i, c := range cols {
pdf.Rect(x, y, widths[i], hHeaderRowH, "DF")
pdf.SetXY(x+hCellPadX, y+1.6)
pdf.CellFormat(widths[i]-2*hCellPadX, hHeaderRowH-3.2, c, "", 0, "C", false, 0, "")
x += widths[i]
}
pdf.Ln(hHeaderRowH)
}
func hDrawMainDataRow(pdf *gofpdf.Fpdf, row []string, widths []float64, rowH float64, rowIndex int) {
x := hMarginL
y := pdf.GetY()
// ✅ Zebra efekti
wTotal := hPageWidth - hMarginL - hMarginR
if rowIndex%2 == 1 {
pdf.SetFillColor(hColorSecondary[0], hColorSecondary[1], hColorSecondary[2])
pdf.Rect(x, y, wTotal, rowH, "F")
}
pdf.SetFont(hFontFamilyReg, "", 8.2)
pdf.SetDrawColor(242, 235, 222)
pdf.SetTextColor(30, 30, 30)
for i, val := range row {
pdf.Rect(x, y, widths[i], rowH, "")
switch {
case i == 4: // Açıklama wrap
hDrawWrapText(pdf, val, x+hCellPadX, y+0.5, widths[i]-2*hCellPadX, hLineHMain)
case i >= 6:
pdf.SetXY(x+hCellPadX, y+(rowH-hLineHMain)/2)
pdf.CellFormat(widths[i]-2*hCellPadX, hLineHMain, val, "", 0, "R", false, 0, "")
default:
pdf.SetXY(x+hCellPadX, y+(rowH-hLineHMain)/2)
pdf.CellFormat(widths[i]-2*hCellPadX, hLineHMain, val, "", 0, "L", false, 0, "")
}
x += widths[i]
}
// ❌ pdf.Ln(rowH) YOK
// ✅ Manuel olarak Y'yi arttırıyoruz
pdf.SetY(y + rowH)
}
/* ============================ MAIN HANDLER ============================ */
func ExportStatementHeaderReportPDFHandler(mssql *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
started := time.Now()
accountCode := r.URL.Query().Get("accountcode")
startDate := r.URL.Query().Get("startdate")
endDate := r.URL.Query().Get("enddate")
rawParis := r.URL.Query()["parislemler"]
var parislemler []string
for _, v := range rawParis {
v = strings.TrimSpace(v)
if v != "" && v != "undefined" && v != "null" {
parislemler = append(parislemler, v)
}
}
headers, _, err := queries.GetStatementsHPDF(accountCode, startDate, endDate, parislemler)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
type grp struct {
code string
rows []models.StatementHeader
sonBakiye float64
}
order := []string{}
groups := map[string]*grp{}
for _, h := range headers {
g, ok := groups[h.ParaBirimi]
if !ok {
g = &grp{code: h.ParaBirimi}
groups[h.ParaBirimi] = g
order = append(order, h.ParaBirimi)
}
g.rows = append(g.rows, h)
}
for _, k := range order {
sort.SliceStable(groups[k].rows, func(i, j int) bool {
ri, rj := groups[k].rows[i], groups[k].rows[j]
if ri.BelgeTarihi == rj.BelgeTarihi {
return ri.BelgeNo < rj.BelgeNo
}
return ri.BelgeTarihi < rj.BelgeTarihi
})
if n := len(groups[k].rows); n > 0 {
groups[k].sonBakiye = groups[k].rows[n-1].Bakiye
}
}
pdf := gofpdf.New("L", "mm", "A4", "")
pdf.SetMargins(hMarginL, hMarginT, hMarginR)
pdf.SetAutoPageBreak(false, hMarginB)
hEnsureFonts(pdf)
wAvail := hPageWidth - hMarginL - hMarginR
mainWn := hNormalizeWidths(hMainWbase, wAvail)
cariIsim := ""
if len(headers) > 0 {
cariIsim = headers[0].CariIsim
}
pageNum := 0
newPage := func() {
pageNum++
pdf.AddPage()
tableTop := hDrawPageHeader(pdf, accountCode, cariIsim, startDate, endDate)
pdf.SetY(tableTop)
}
newPage()
for _, cur := range order {
g := groups[cur]
hDrawGroupBar(pdf, cur, g.sonBakiye)
hDrawMainHeaderRow(pdf, hMainCols, mainWn)
rowIndex := 0
for _, h := range g.rows {
row := []string{
h.BelgeNo, h.BelgeTarihi, h.VadeTarihi, h.IslemTipi,
h.Aciklama, h.ParaBirimi,
hFormatCurrencyTR(h.Borc),
hFormatCurrencyTR(h.Alacak),
hFormatCurrencyTR(h.Bakiye),
}
rh := hCalcRowHeightForText(pdf, row[4], mainWn[4], hLineHMain, hCellPadX)
if hNeedNewPage(pdf, rh+hHeaderRowH) {
newPage()
hDrawGroupBar(pdf, cur, g.sonBakiye)
hDrawMainHeaderRow(pdf, hMainCols, mainWn)
}
hDrawMainDataRow(pdf, row, mainWn, rh, rowIndex)
rowIndex++
}
pdf.Ln(1)
}
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
http.Error(w, "PDF oluşturulamadı: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Disposition", "inline; filename=statement-header-report.pdf")
_, _ = w.Write(buf.Bytes())
log.Printf("✅ Header-only PDF üretimi tamamlandı (%s)", time.Since(started))
}
}

View File

@@ -0,0 +1,642 @@
// routes/statements_pdf.go
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/models"
"bssapp-backend/queries"
"bytes"
"database/sql"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/jung-kurt/gofpdf"
)
/* ============================ SABİTLER ============================ */
// A4 Landscape (mm)
const (
pageWidth = 297.0
pageHeight = 210.0
// Kenar boşlukları (mm)
marginL = 8.0
marginT = 8.0
marginR = 8.0
marginB = 15.0
// Satır aralıkları (mm)
lineHMain = 6.0 // ana satır biraz iri
lineHDetail = 5.2
cellPadX = 2.2
// Satır yükseklikleri (mm)
headerRowH = 8.4
subHeaderRowH = 6.2
groupBarH = 9.0
// Çizgi kalınlığı
gridLineWidth = 0.3
// Logo genişliği (yükseklik oranlanır)
logoW = 42.0
)
// Ana tablo kolonları
var mainCols = []string{
"Belge No", "Tarih", "Vade", "İşlem",
"Açıklama", "Para", "Borç", "Alacak", "Bakiye",
}
// Ana tablo kolon genişlikleri (ilk 3 geniş)
var mainWbase = []float64{
34, // Belge No
30, // Tarih
28, // Vade
24, // İşlem
60, // Açıklama (biraz daraltıldı)
20, // Para
36, // Borç (genişletildi)
36, // Alacak (genişletildi)
36, // Bakiye (genişletildi)
}
// Detay tablo kolonları ve genişlikleri
var dCols = []string{
"Ana Grup", "Alt Grup", "Garson", "Fit", "İçerik",
"Ürün", "Renk", "Adet", "Fiyat", "Tutar",
}
var dWbase = []float64{
30, 28, 22, 20, 56, 30, 22, 20, 20, 26}
// Font dosyaları
const (
fontFamilyReg = "dejavu"
fontFamilyBold = "dejavu-b"
fontPathReg = "fonts/DejaVuSans.ttf"
fontPathBold = "fonts/DejaVuSans-Bold.ttf"
)
// Kurumsal renkler
var (
colorPrimary = [3]int{149, 113, 22} // #957116 (altın)
colorSecondary = [3]int{218, 193, 151} // #dac197 (bej)
colorDetailFill = [3]int{242, 235, 222} // secondaryden daha açık (detay satır zemin)
)
/* ============================ HELPERS ============================ */
// Genişlikleri normalize et
func normalizeWidths(base []float64, targetTotal float64) []float64 {
sum := 0.0
for _, v := range base {
sum += v
}
out := make([]float64, len(base))
if sum <= 0 {
each := targetTotal / float64(len(base))
for i := range out {
out[i] = each
}
return out
}
scale := targetTotal / sum
for i, v := range base {
out[i] = v * scale
}
return out
}
// Türkçe formatlı sayı
func formatCurrencyTR(n float64) string {
s := fmt.Sprintf("%.2f", n) // "864000.00"
parts := strings.Split(s, ".")
intPart := parts[0]
decPart := parts[1]
// Binlik ayırıcı (.)
var out strings.Builder
for i, c := range intPart {
if (len(intPart)-i)%3 == 0 && i != 0 {
out.WriteRune('.')
}
out.WriteRune(c)
}
return out.String() + "," + decPart
}
// Fontları yükle
func ensureFonts(pdf *gofpdf.Fpdf) {
if _, err := os.Stat(fontPathReg); err == nil {
pdf.AddUTF8Font(fontFamilyReg, "", fontPathReg)
} else {
log.Printf("⚠️ Font bulunamadı: %s", fontPathReg)
}
if _, err := os.Stat(fontPathBold); err == nil {
pdf.AddUTF8Font(fontFamilyBold, "", fontPathBold)
} else {
log.Printf("⚠️ Font bulunamadı: %s", fontPathBold)
}
}
// Güvenli satır kırma
func splitLinesSafe(pdf *gofpdf.Fpdf, text string, width float64) [][]byte {
if width <= 0 {
width = 1
}
defer func() {
if rec := recover(); rec != nil {
log.Printf("⚠️ splitLinesSafe recover: %v", rec)
}
}()
return pdf.SplitLines([]byte(text), width)
}
// Metin wrap çizimi
func drawWrapText(pdf *gofpdf.Fpdf, text string, x, y, width, lineH float64) {
if text == "" {
return
}
lines := splitLinesSafe(pdf, text, width)
cy := y
for _, ln := range lines {
pdf.SetXY(x, cy)
pdf.CellFormat(width, lineH, string(ln), "", 0, "L", false, 0, "")
cy += lineH
}
}
// Wrap satır yüksekliği
func calcRowHeightForText(pdf *gofpdf.Fpdf, text string, colWidth, lineHeight, padX float64) float64 {
if colWidth <= 2*padX {
colWidth = 2*padX + 1
}
if text == "" {
return lineHeight
}
lines := splitLinesSafe(pdf, text, colWidth-(2*padX))
if len(lines) == 0 {
return lineHeight
}
h := float64(len(lines)) * lineHeight
if h < lineHeight {
h = lineHeight
}
return h
}
// Alt tarafa sığmıyor mu?
func needNewPage(pdf *gofpdf.Fpdf, needH float64) bool {
return pdf.GetY()+needH+marginB > pageHeight
}
// NULL/boşları uzun çizgiye çevir
func nullToDash(s string) string {
if strings.TrimSpace(s) == "" {
return "—"
}
return s
}
/* ============================ HEADER DRAW ============================ */
// Küçük yardımcı: kutu içine otomatik 1-2 satır metin (taşarsa 2)
func drawLabeledBox(pdf *gofpdf.Fpdf, x, y, w, h float64, label, value string, align string) {
// Çerçeve
pdf.Rect(x, y, w, h, "")
// İç marj
ix := x + 2
iy := y + 1.8
iw := w - 4
// Etiket (kalın, küçük)
pdf.SetFont(fontFamilyBold, "", 8)
pdf.SetXY(ix, iy)
pdf.CellFormat(iw, 4, label, "", 0, "L", false, 0, "")
// Değer (normal, 1-2 satır)
pdf.SetFont(fontFamilyReg, "", 8)
vy := iy + 4.2
lineH := 4.2
lines := splitLinesSafe(pdf, value, iw)
if len(lines) > 2 {
lines = lines[:2] // en fazla 2 satır göster
}
for _, ln := range lines {
pdf.SetXY(ix, vy)
pdf.CellFormat(iw, lineH, string(ln), "", 0, align, false, 0, "")
vy += lineH
}
}
func drawPageHeader(pdf *gofpdf.Fpdf, cariKod, cariIsim, start, end string) float64 {
logoPath, _ := filepath.Abs("./public/Baggi-Tekstil-A.s-Logolu.jpeg")
// Logo
pdf.ImageOptions(logoPath, hMarginL, 2, hLogoW, 0, false, gofpdf.ImageOptions{}, 0, "")
// Başlıklar
pdf.SetFont(hFontFamilyBold, "", 16)
pdf.SetTextColor(hColorPrimary[0], hColorPrimary[1], hColorPrimary[2])
pdf.SetXY(hMarginL+hLogoW+8, hMarginT+2)
pdf.CellFormat(120, 7, "Baggi Software System", "", 0, "L", false, 0, "")
pdf.SetFont(hFontFamilyBold, "", 12)
pdf.SetXY(hMarginL+hLogoW+8, hMarginT+10)
pdf.CellFormat(120, 6, "Cari Hesap Raporu", "", 0, "L", false, 0, "")
// Bugünün tarihi (sağ üst)
today := time.Now().Format("02.01.2006")
pdf.SetFont(hFontFamilyReg, "", 9)
pdf.SetXY(hPageWidth-hMarginR-40, hMarginT+3)
pdf.CellFormat(40, 6, "Tarih: "+today, "", 0, "R", false, 0, "")
// Cari & Tarih kutuları (daha yukarı taşındı)
boxY := hMarginT + hLogoW - 6
pdf.SetFont(hFontFamilyBold, "", 10)
pdf.Rect(hMarginL, boxY, 140, 11, "")
pdf.SetXY(hMarginL+2, boxY+3)
pdf.CellFormat(136, 5, fmt.Sprintf("Cari: %s — %s", cariKod, cariIsim), "", 0, "L", false, 0, "")
pdf.Rect(hPageWidth-hMarginR-140, boxY, 140, 11, "")
pdf.SetXY(hPageWidth-hMarginR-138, boxY+3)
pdf.CellFormat(136, 5, fmt.Sprintf("Tarih Aralığı: %s → %s", start, end), "", 0, "R", false, 0, "")
// Alt çizgi
y := boxY + 13
pdf.SetDrawColor(hColorPrimary[0], hColorPrimary[1], hColorPrimary[2])
pdf.Line(hMarginL, y, hPageWidth-hMarginR, y)
pdf.SetDrawColor(200, 200, 200)
return y + 4
}
/* ============================ GROUP BAR ============================ */
func drawGroupBar(pdf *gofpdf.Fpdf, currency string, sonBakiye float64) {
// Kutu alanı (tam genişlik)
x := marginL
y := pdf.GetY()
w := pageWidth - marginL - marginR
h := groupBarH
// Çerçeve
pdf.SetDrawColor(colorPrimary[0], colorPrimary[1], colorPrimary[2])
pdf.SetLineWidth(0.6)
pdf.Rect(x, y, w, h, "")
// Metinler
pdf.SetFont(fontFamilyBold, "", 11.3) // bir tık büyük
pdf.SetTextColor(colorPrimary[0], colorPrimary[1], colorPrimary[2])
pdf.SetXY(x+cellPadX+1.0, y+(h-5.0)/2)
pdf.CellFormat(w*0.6, 5.0, fmt.Sprintf("%s", currency), "", 0, "L", false, 0, "")
txt := "Son Bakiye = " + formatCurrencyTR(sonBakiye)
pdf.SetXY(x+w*0.4, y+(h-5.0)/2)
pdf.CellFormat(w*0.6-2.0, 5.0, txt, "", 0, "R", false, 0, "")
// Renk/kalınlık geri
pdf.SetLineWidth(gridLineWidth)
pdf.SetDrawColor(200, 200, 200)
pdf.SetTextColor(0, 0, 0)
pdf.Ln(h)
}
/* ============================ HEADER ROWS ============================ */
func drawMainHeaderRow(pdf *gofpdf.Fpdf, cols []string, widths []float64) {
// Ana başlık: Primary arkaplan, beyaz yazı
pdf.SetFont(fontFamilyBold, "", 10.2)
pdf.SetTextColor(255, 255, 255)
pdf.SetFillColor(colorPrimary[0], colorPrimary[1], colorPrimary[2])
pdf.SetDrawColor(120, 90, 20)
x := marginL
y := pdf.GetY()
for i, c := range cols {
pdf.Rect(x, y, widths[i], headerRowH, "DF")
pdf.SetXY(x+cellPadX, y+1.6)
pdf.CellFormat(widths[i]-2*cellPadX, headerRowH-3.2, c, "", 0, "C", false, 0, "")
x += widths[i]
}
pdf.SetY(y + headerRowH)
}
func drawDetailHeaderRow(pdf *gofpdf.Fpdf, cols []string, widths []float64) {
// Detay başlık: Secondary (daha açık)
pdf.SetFont(fontFamilyBold, "", 9.2)
pdf.SetFillColor(colorSecondary[0], colorSecondary[1], colorSecondary[2])
pdf.SetTextColor(0, 0, 0)
pdf.SetDrawColor(160, 140, 100)
x := marginL
y := pdf.GetY()
for i, c := range cols {
pdf.Rect(x, y, widths[i], subHeaderRowH, "DF")
pdf.SetXY(x+cellPadX, y+1.2)
pdf.CellFormat(widths[i]-2*cellPadX, subHeaderRowH-2.4, c, "", 0, "C", false, 0, "")
x += widths[i]
}
pdf.Ln(subHeaderRowH)
}
/* ============================ DATA ROWS ============================ */
// Ana veri satırı: daha büyük ve kalın görünsün
// Ana veri satırı (zebrasız, sıkı grid satırlar yapışık)
func drawMainDataRow(pdf *gofpdf.Fpdf, row []string, widths []float64, rowH float64) {
x := marginL
y := pdf.GetY()
// Arka plan beyaz, çizgiler gri
pdf.SetFont(fontFamilyBold, "", 8.5)
pdf.SetDrawColor(210, 210, 210)
pdf.SetTextColor(30, 30, 30)
pdf.SetFillColor(255, 255, 255)
for i, val := range row {
// Hücre çerçevesi
pdf.Rect(x, y, widths[i], rowH, "")
switch {
case i == 4: // Açıklama wrap
drawWrapText(pdf, val, x+cellPadX, y+0.8, widths[i]-2*cellPadX, lineHMain)
case i >= 6: // Sayısal sağ
pdf.SetXY(x+cellPadX, y+(rowH-lineHMain)/2)
pdf.CellFormat(widths[i]-2*cellPadX, lineHMain, val, "", 0, "R", false, 0, "")
default: // Normal sol
pdf.SetXY(x+cellPadX, y+(rowH-lineHMain)/2)
pdf.CellFormat(widths[i]-2*cellPadX, lineHMain, val, "", 0, "L", false, 0, "")
}
x += widths[i]
}
// ❌ pdf.Ln(rowH) yerine:
// ✅ bir alt satıra tam yapışık in
pdf.SetY(y + rowH)
}
// Detay veri satırı: açık zemin, biraz küçük; zebra opsiyonel
func drawDetailDataRow(pdf *gofpdf.Fpdf, row []string, widths []float64, rowH float64, fillBg bool) {
pdf.SetFont(fontFamilyReg, "", 8.0)
pdf.SetTextColor(60, 60, 60)
pdf.SetDrawColor(220, 220, 220)
x := marginL
y := pdf.GetY()
for i, val := range row {
// Zemin
if fillBg {
pdf.SetFillColor(colorDetailFill[0], colorDetailFill[1], colorDetailFill[2])
pdf.Rect(x, y, widths[i], rowH, "DF")
pdf.SetFillColor(255, 255, 255) // geri
} else {
pdf.Rect(x, y, widths[i], rowH, "")
}
switch {
case i == 4: // İçerik wrap
drawWrapText(pdf, val, x+cellPadX, y+1.4, widths[i]-2*cellPadX, lineHDetail)
case i >= 7: // Sayısal sağ
pdf.SetXY(x+cellPadX, y+(rowH-lineHDetail)/2)
pdf.CellFormat(widths[i]-2*cellPadX, lineHDetail, val, "", 0, "R", false, 0, "")
default: // Sol
pdf.SetXY(x+cellPadX, y+(rowH-lineHDetail)/2)
pdf.CellFormat(widths[i]-2*cellPadX, lineHDetail, val, "", 0, "L", false, 0, "")
}
x += widths[i]
}
pdf.Ln(rowH)
}
/* ============================ HANDLER ============================ */
func ExportPDFHandler(mssql *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
started := time.Now()
defer func() {
if rec := recover(); rec != nil {
log.Printf("❌ PANIC ExportPDFHandler: %v", rec)
http.Error(w, "PDF oluşturulurken hata oluştu", http.StatusInternalServerError)
}
}()
accountCode := r.URL.Query().Get("accountcode")
startDate := r.URL.Query().Get("startdate")
endDate := r.URL.Query().Get("enddate")
// parislemler sanitize
rawParis := r.URL.Query()["parislemler"]
parislemler := make([]string, 0, len(rawParis))
for _, v := range rawParis {
v = strings.TrimSpace(v)
if v == "" || strings.EqualFold(v, "undefined") || strings.EqualFold(v, "null") {
continue
}
parislemler = append(parislemler, v)
}
log.Printf("▶️ ExportPDFHandler: account=%s start=%s end=%s parislemler=%v",
accountCode, startDate, endDate, parislemler)
// 1) Header verileri
headers, belgeNos, err := queries.GetStatementsPDF(accountCode, startDate, endDate, parislemler)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Printf("✅ Header verileri alındı: %d kayıt, %d belge no", len(headers), len(belgeNos))
// 2) Detay verileri
detailMap, err := queries.GetDetailsMapPDF(belgeNos, startDate, endDate)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Printf("✅ Detay verileri alındı: %d belge için detay var", len(detailMap))
// 3) Gruplama
type grp struct {
code string
rows []models.StatementHeader
sonBakiye float64
}
order := []string{}
groups := map[string]*grp{}
for _, h := range headers {
g, ok := groups[h.ParaBirimi]
if !ok {
g = &grp{code: h.ParaBirimi}
groups[h.ParaBirimi] = g
order = append(order, h.ParaBirimi)
}
g.rows = append(g.rows, h)
}
for _, k := range order {
sort.SliceStable(groups[k].rows, func(i, j int) bool {
ri, rj := groups[k].rows[i], groups[k].rows[j]
if ri.BelgeTarihi == rj.BelgeTarihi {
return ri.BelgeNo < rj.BelgeNo
}
return ri.BelgeTarihi < rj.BelgeTarihi
})
if n := len(groups[k].rows); n > 0 {
groups[k].sonBakiye = groups[k].rows[n-1].Bakiye
}
}
// 4) Kolon genişlikleri
wAvail := pageWidth - marginL - marginR
mainWn := normalizeWidths(mainWbase, wAvail)
dWn := normalizeWidths(dWbase, wAvail)
// 5) PDF init
pdf := gofpdf.New("L", "mm", "A4", "")
pdf.SetMargins(marginL, marginT, marginR)
pdf.SetAutoPageBreak(false, marginB)
ensureFonts(pdf)
pdf.SetFont(fontFamilyReg, "", 8.5)
pdf.SetTextColor(0, 0, 0)
pageNum := 0
cariIsim := ""
if len(headers) > 0 {
cariIsim = headers[0].CariIsim
}
// Sayfa başlatıcı (header yüksekliği dinamik)
newPage := func() {
pageNum++
pdf.AddPage()
// drawPageHeader tablo başlangıç yüksekliğini döndürüyor
tableTop := drawPageHeader(pdf, accountCode, cariIsim, startDate, endDate)
// Sayfa numarası
pdf.SetFont(fontFamilyReg, "", 6)
pdf.SetXY(pageWidth-marginR-28, pageHeight-marginB+3)
pdf.CellFormat(28, 5, fmt.Sprintf("Sayfa %d", pageNum), "", 0, "R", false, 0, "")
// Tablo Y konumunu ayarla
pdf.SetY(tableTop)
}
newPage()
// 6) Gruplar yaz
for _, cur := range order {
g := groups[cur]
if needNewPage(pdf, groupBarH+headerRowH) {
newPage()
}
drawGroupBar(pdf, cur, g.sonBakiye)
drawMainHeaderRow(pdf, mainCols, mainWn)
for _, h := range g.rows {
row := []string{
h.BelgeNo, h.BelgeTarihi, h.VadeTarihi, h.IslemTipi,
h.Aciklama, h.ParaBirimi,
formatCurrencyTR(h.Borc),
formatCurrencyTR(h.Alacak),
formatCurrencyTR(h.Bakiye),
}
pdf.SetFont(fontFamilyBold, "", 9.6)
rh := calcRowHeightForText(pdf, row[4], mainWn[4], lineHMain, cellPadX)
if needNewPage(pdf, rh+headerRowH) {
newPage()
drawGroupBar(pdf, cur, g.sonBakiye)
drawMainHeaderRow(pdf, mainCols, mainWn)
}
drawMainDataRow(pdf, row, mainWn, rh)
// detaylar
details := detailMap[h.BelgeNo]
if len(details) > 0 {
if needNewPage(pdf, subHeaderRowH) {
newPage()
drawGroupBar(pdf, cur, g.sonBakiye)
drawMainHeaderRow(pdf, mainCols, mainWn)
}
drawDetailHeaderRow(pdf, dCols, dWn)
for i, d := range details {
drow := []string{
nullToDash(d.UrunAnaGrubu),
nullToDash(d.UrunAltGrubu),
nullToDash(d.YetiskinGarson),
nullToDash(d.Fit),
nullToDash(d.Icerik),
nullToDash(d.UrunKodu),
nullToDash(d.UrunRengi),
formatCurrencyTR(d.ToplamAdet),
formatCurrencyTR(d.ToplamFiyat),
formatCurrencyTR(d.ToplamTutar),
}
pdf.SetFont(fontFamilyReg, "", 8)
rh2 := calcRowHeightForText(pdf, drow[4], dWn[4], lineHDetail, cellPadX)
if needNewPage(pdf, rh2) {
newPage()
drawGroupBar(pdf, cur, g.sonBakiye)
drawMainHeaderRow(pdf, mainCols, mainWn)
drawDetailHeaderRow(pdf, dCols, dWn)
}
// zebra: çift indekslerde açık zemin
fill := (i%2 == 0)
drawDetailDataRow(pdf, drow, dWn, rh2, fill)
}
}
}
pdf.Ln(3)
}
// 7) Çıktı
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
http.Error(w, "PDF oluşturulamadı: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Disposition", "inline; filename=statement.pdf")
_, _ = w.Write(buf.Bytes())
log.Printf("✅ PDF üretimi tamam: %s", time.Since(started))
}
}
/*
NOTLAR:
- Header artık dinamik yüksekliğe sahip (drawPageHeader -> contentTopY döner).
- Logo sol üst köşe, başlık “toolbar” gibi; “Cari / Tarih” kutuları
12 satır metni çerçeve içinde otomatik kırar.
- Grup barı primary renkte çerçeveli ve yazı biraz büyük.
- Ana satırlar kalın & bir tık büyük (detaydan farklı).
- Detay satırlar açık (bej) zeminle zebra (i%2==0) uygulanır.
- SplitLines her yerde splitLinesSafe ile korunuyor (panic yok).
- AddPage sadece newPage()de.
- Font bulunamazsa gömme fontlarla devam edilir (log yazar).
*/

49
svc/routes/test_mail.go Normal file
View File

@@ -0,0 +1,49 @@
package routes
import (
"context"
"encoding/json"
"net/http"
"bssapp-backend/auth"
"bssapp-backend/internal/mailer"
)
func TestMailHandler(ml *mailer.GraphMailer) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
var req struct {
To string `json:"to"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
err := ml.Send(
context.Background(),
mailer.Message{
To: []string{req.To},
Subject: "BSSApp Test Mail",
BodyHTML: "<h3>🎉 Test mail başarılı</h3><p>Microsoft Graph üzerinden gönderildi.</p>",
},
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(map[string]string{
"message": "Mail gönderildi",
})
})
}

View File

@@ -0,0 +1,30 @@
package routes
import (
"bssapp-backend/queries"
"database/sql"
"encoding/json"
"fmt"
"net/http"
)
// ✅ Handler artık parametre alıyor
func GetTodayCurrencyV3Handler(mssql *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
if code == "" {
http.Error(w, "Eksik parametre: code", http.StatusBadRequest)
return
}
currency, err := queries.GetTodayCurrencyV3(mssql, code) // 👈 MSSQL
if err != nil {
http.Error(w, fmt.Sprintf("Kur bulunamadı: %v", err), http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(currency)
}
}

320
svc/routes/user_detail.go Normal file
View File

@@ -0,0 +1,320 @@
package routes
import (
"bssapp-backend/internal/auditlog"
"bssapp-backend/internal/mailer"
"bssapp-backend/internal/security"
"bssapp-backend/models"
"bssapp-backend/queries"
"bytes"
"database/sql"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
"github.com/gorilla/mux"
)
// ======================================================
// 👤 USER DETAIL ROUTE (GET + PUT)
// URL: /api/users/{id}
// ======================================================
type LoginUser struct {
ID int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
RoleID int `json:"role_id"`
RoleCode string `json:"role_code"`
ForcePasswordChange bool `json:"force_password_change"`
}
func UserDetailRoute(db *sql.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
idStr := mux.Vars(r)["id"]
userID, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || userID <= 0 {
http.Error(w, "Geçersiz kullanıcı id", http.StatusBadRequest)
return
}
switch r.Method {
case http.MethodGet:
handleUserGet(db, w, userID)
case http.MethodPut:
handleUserUpdate(db, w, r, userID)
case http.MethodOptions:
w.WriteHeader(http.StatusOK)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
}
// ======================================================
// 📥 GET USER
// ======================================================
// ======================================================
// 📥 GET USER (FINAL - mk_dfusr Uyumlu)
// ======================================================
func handleUserGet(db *sql.DB, w http.ResponseWriter, userID int64) {
var u models.UserDetail
// --------------------------------------------------
// 🟢 HEADER
// --------------------------------------------------
err := db.QueryRow(
queries.GetUserHeader,
userID,
).Scan(
&u.ID,
&u.Code, // username
&u.IsActive,
&u.FullName,
&u.Email,
&u.Mobile,
&u.Address,
&u.HasPassword,
)
if err != nil {
// detaylı log (çok önemli)
fmt.Println("❌ [UserDetail] HEADER SCAN ERROR:", err)
if err == sql.ErrNoRows {
http.Error(w, "Kullanıcı bulunamadı", http.StatusNotFound)
return
}
http.Error(w, "User header error", http.StatusInternalServerError)
return
}
// --------------------------------------------------
// 🟢 ROLES
// --------------------------------------------------
roleRows, err := db.Query(queries.GetUserRoles, userID)
if err != nil {
fmt.Println("⚠️ [UserDetail] ROLE QUERY:", err)
} else {
defer roleRows.Close()
for roleRows.Next() {
var code string
if err := roleRows.Scan(&code); err == nil {
u.Roles = append(u.Roles, code)
}
}
}
// --------------------------------------------------
// 🟢 DEPARTMENTS
// --------------------------------------------------
deptRows, err := db.Query(queries.GetUserDepartments, userID)
if err != nil {
fmt.Println("⚠️ [UserDetail] DEPT QUERY:", err)
} else {
defer deptRows.Close()
for deptRows.Next() {
var d models.DeptOption
if err := deptRows.Scan(&d.Code, &d.Title); err == nil {
u.Departments = append(u.Departments, d)
}
}
}
// --------------------------------------------------
// 🟢 PIYASALAR
// --------------------------------------------------
piyRows, err := db.Query(queries.GetUserPiyasalar, userID)
if err != nil {
fmt.Println("⚠️ [UserDetail] PIYASA QUERY:", err)
} else {
defer piyRows.Close()
for piyRows.Next() {
var p models.DeptOption
if err := piyRows.Scan(&p.Code, &p.Title); err == nil {
u.Piyasalar = append(u.Piyasalar, p)
}
}
}
// --------------------------------------------------
// 🟢 NEBIM USERS
// --------------------------------------------------
nebimRows, err := db.Query(queries.GetUserNebim, userID)
if err != nil {
fmt.Println("⚠️ [UserDetail] NEBIM QUERY:", err)
} else {
defer nebimRows.Close()
for nebimRows.Next() {
var n models.NebimOption
if err := nebimRows.Scan(
&n.Username,
&n.UserGroupCode,
); err == nil {
u.NebimUsers = append(u.NebimUsers, n)
}
}
}
// --------------------------------------------------
// 🟢 RESPONSE
// --------------------------------------------------
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(u); err != nil {
fmt.Println("❌ [UserDetail] JSON ENCODE:", err)
}
}
// ======================================================
// ✍️ UPDATE USER (PUT)
// ======================================================
func handleUserUpdate(db *sql.DB, w http.ResponseWriter, r *http.Request, userID int64) {
raw, _ := io.ReadAll(r.Body)
fmt.Println("🟥 RAW BODY:", string(raw))
// body tekrar okunabilsin diye reset
r.Body = io.NopCloser(bytes.NewBuffer(raw))
var payload models.UserWrite
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
fmt.Printf("🟦 DECODED PAYLOAD: %+v\n", payload)
fmt.Println("❌ JSON DECODE ERROR:", err)
http.Error(w, "Geçersiz payload", http.StatusBadRequest)
return
}
tx, err := db.Begin()
if err != nil {
http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError)
return
}
defer tx.Rollback()
_, err = tx.Exec(
queries.UpdateUserHeader,
userID,
payload.Code,
payload.IsActive,
payload.FullName,
payload.Email,
payload.Mobile,
payload.Address,
)
if err != nil {
http.Error(w, "Header güncellenemedi", http.StatusInternalServerError)
return
}
tx.Exec(`DELETE FROM dfrole_usr WHERE dfusr_id = $1`, userID)
for _, code := range payload.Roles {
tx.Exec(queries.InsertUserRole, userID, code)
}
tx.Exec(`DELETE FROM dfusr_dprt WHERE dfusr_id = $1`, userID)
for _, d := range payload.Departments {
tx.Exec(queries.InsertUserDepartment, userID, d.Code)
}
tx.Exec(`DELETE FROM dfusr_piyasa WHERE dfusr_id = $1`, userID)
for _, p := range payload.Piyasalar {
tx.Exec(queries.InsertUserPiyasa, userID, p.Code)
}
tx.Exec(`DELETE FROM dfusr_nebim_user WHERE dfusr_id = $1`, userID)
for _, n := range payload.NebimUsers {
tx.Exec(queries.InsertUserNebim, userID, n.Username)
}
if err := tx.Commit(); err != nil {
http.Error(w, "Commit başarısız", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"success": true})
}
// ======================================================
// 🔐 ADMIN — PASSWORD RESET MAIL
// ======================================================
func SendPasswordResetMailHandler(
db *sql.DB,
mailer *mailer.GraphMailer,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, _ := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
var email string
err := db.QueryRow(`
SELECT email
FROM mk_dfusr
WHERE id = $1 AND is_active = true
`, userID).Scan(&email)
if err != nil || email == "" {
w.WriteHeader(http.StatusOK)
return
}
// 🔑 TOKEN (PLAIN + HASH)
plain, hash, err := security.GenerateResetToken()
if err != nil {
w.WriteHeader(http.StatusOK)
return
}
expires := time.Now().Add(30 * time.Minute)
// 💾 DB → SADECE HASH
_, _ = db.Exec(`
INSERT INTO dfusr_password_reset (dfusr_id, token, expires_at)
VALUES ($1,$2,$3)
`, userID, hash, expires)
// 🔗 URL → PLAIN
resetURL := fmt.Sprintf(
"%s/password-reset/%s",
os.Getenv("FRONTEND_URL"),
plain,
)
_ = mailer.SendPasswordResetMail(email, resetURL)
// 🕵️ AUDIT
auditlog.ForcePasswordChangeStarted(
r.Context(),
userID,
"admin_reset",
)
w.WriteHeader(http.StatusOK)
}
}

81
svc/routes/user_list.go Normal file
View File

@@ -0,0 +1,81 @@
package routes
import (
"bssapp-backend/models"
"bssapp-backend/queries"
"database/sql"
"encoding/json"
"log"
"net/http"
)
// ======================================================
// 📌 UserListRoute — Kullanıcı Listeleme API (FINAL)
// ======================================================
func UserListRoute(pg *sql.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
// --------------------------------------------------
// 🔍 Query Param
// --------------------------------------------------
search := r.URL.Query().Get("search")
log.Printf("📥 /api/users/list çağrıldı | search='%s'", search)
// --------------------------------------------------
// 🗄️ SQL CALL
// --------------------------------------------------
rows, err := queries.GetUserList(pg, search)
if err != nil {
log.Printf("❌ SQL sorgu hatası (GetUserList): %v", err)
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
return
}
defer rows.Close()
// --------------------------------------------------
// 📦 Sonuç Listesi
// --------------------------------------------------
list := make([]models.UserListRow, 0, 100)
count := 0
// ==================================================
// 🧠 SCAN — SQL SELECT ile BİRE BİR (8 kolon)
// ==================================================
for rows.Next() {
var u models.UserListRow
err = rows.Scan(
&u.ID, // 1
&u.Code, // 2
&u.IsActive, // 3
&u.NebimUsername, // 4 (nullable)
&u.UserGroupCode, // 5 (nullable)
&u.RoleNames, // 6
&u.DepartmentNames, // 7
&u.PiyasaNames, // 8
)
if err != nil {
log.Printf("⚠️ Satır atlandı (SCAN hatası): %v", err)
continue
}
list = append(list, u)
count++
}
if err := rows.Err(); err != nil {
log.Printf("⚠️ rows.Err(): %v", err)
}
log.Printf("✅ User listesi tamamlandı — %d kayıt gönderildi.", count)
// --------------------------------------------------
// 📤 JSON OUTPUT
// --------------------------------------------------
_ = json.NewEncoder(w).Encode(list)
})
}

View File

@@ -0,0 +1,232 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/internal/auditlog"
"bssapp-backend/permissions"
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"github.com/gorilla/mux"
)
/* =====================================================
GET USER OVERRIDES
GET /api/users/{id}/permissions
===================================================== */
func GetUserPermissionsHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// ✅ Auth kontrolü yeterli (Yetki middleware'de)
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
idStr := mux.Vars(r)["id"]
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
repo := permissions.NewPermissionRepository(db)
rows, err := repo.GetUserOverridesByUserID(id)
if err != nil {
http.Error(w, "db error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(rows)
}
}
/* =====================================================
SAVE USER OVERRIDES
POST /api/users/{id}/permissions
===================================================== */
func SaveUserPermissionsHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// ✅ Auth kontrolü yeterli
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
userIDStr := mux.Vars(r)["id"]
userID, err := strconv.ParseInt(userIDStr, 10, 64)
if err != nil || userID <= 0 {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
/* ================= PAYLOAD ================= */
var list []permissions.UserPermissionRequest
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
http.Error(w, "bad payload", http.StatusBadRequest)
return
}
repo := permissions.NewPermissionRepository(db)
/* ================= OLD ================= */
oldRows, _ := repo.GetUserOverridesByUserID(userID)
oldMap := map[string]bool{}
for _, p := range oldRows {
key := p.Module + ":" + p.Action
oldMap[key] = p.Allowed
}
/* ================= NEW ================= */
newMap := map[string]bool{}
for _, p := range list {
key := p.Module + ":" + p.Action
newMap[key] = p.Allowed
}
/* ================= DIFF ================= */
var changes []map[string]any
for key, newVal := range newMap {
oldVal := oldMap[key]
if oldVal != newVal {
parts := strings.Split(key, ":")
if len(parts) != 2 {
continue
}
changes = append(changes, map[string]any{
"module": parts[0],
"action": parts[1],
"before": oldVal,
"after": newVal,
})
}
}
/* ================= SAVE ================= */
if err := repo.SaveUserOverrides(userID, list); err != nil {
http.Error(w, "db error", http.StatusInternalServerError)
return
}
/* ================= AUDIT ================= */
if len(changes) > 0 && claims != nil {
var targetUsername string
_ = db.QueryRow(`
SELECT username
FROM mk_dfusr
WHERE id = $1
`, userID).Scan(&targetUsername)
auditlog.Enqueue(r.Context(), auditlog.ActivityLog{
ActionType: "user_permission_change",
ActionCategory: "permission",
ActionTarget: fmt.Sprintf(
"/api/users/%d/permissions",
userID,
),
Description: "user permission overrides updated",
Username: claims.Username,
RoleCode: claims.RoleCode,
DfUsrID: int64(claims.ID),
TargetDfUsrID: userID,
TargetUsername: targetUsername,
ChangeBefore: map[string]any{
"permissions": oldRows,
},
ChangeAfter: map[string]any{
"changes": changes,
},
IsSuccess: true,
})
}
/* ================= RESPONSE ================= */
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"success": true,
})
}
}
/* =====================================================
OPTIONAL METHOD
===================================================== */
func (h *PermissionHandler) SaveUserOverrides(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
userID, _ := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
var list []permissions.UserPermissionRequest
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
http.Error(w, "bad payload", 400)
return
}
log.Println("➡️ SAVE USER PERMISSIONS")
log.Println("USER ID:", userID)
log.Println("ITEM COUNT:", len(list))
if err := h.Repo.SaveUserOverrides(userID, list); err != nil {
log.Println("🔥 USER PERMISSION SAVE ERROR")
log.Println("ERROR:", err)
http.Error(w, err.Error(), 500)
return
}
json.NewEncoder(w).Encode(map[string]bool{
"success": true,
})
}