ilk
This commit is contained in:
39
svc/routes/account.go
Normal file
39
svc/routes/account.go
Normal 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
126
svc/routes/activitylogs.go
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
61
svc/routes/admin_piyasa.go
Normal file
61
svc/routes/admin_piyasa.go
Normal 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"}`))
|
||||
}
|
||||
104
svc/routes/admin_reset_password.go
Normal file
104
svc/routes/admin_reset_password.go
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
60
svc/routes/audit_helper.go
Normal file
60
svc/routes/audit_helper.go
Normal 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)
|
||||
}
|
||||
92
svc/routes/auth_refresh.go
Normal file
92
svc/routes/auth_refresh.go
Normal 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,
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
57
svc/routes/customerlist.go
Normal file
57
svc/routes/customerlist.go
Normal 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),
|
||||
)
|
||||
}
|
||||
169
svc/routes/first_password_change.go
Normal file
169
svc/routes/first_password_change.go
Normal 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(¤tHash)
|
||||
|
||||
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
733
svc/routes/login.go
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
91
svc/routes/order_list_excel.go
Normal file
91
svc/routes/order_list_excel.go
Normal 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
1387
svc/routes/order_pdf.go
Normal 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) // wrap’te kullanılacak font
|
||||
lineH := 4.0
|
||||
textW := pageW - marginL*2 - 52
|
||||
|
||||
// Kaç satır olacağını hesapla
|
||||
lines := pdf.SplitLines([]byte(text), textW)
|
||||
descBoxH := float64(len(lines))*lineH + 4 // min boşluk
|
||||
|
||||
if descBoxH < 10 {
|
||||
descBoxH = 10
|
||||
}
|
||||
|
||||
pdf.SetDrawColor(210, 210, 210)
|
||||
pdf.Rect(marginL, y, pageW-marginL*2, descBoxH, "")
|
||||
|
||||
// Başlık
|
||||
pdf.SetFont("dejavu-b", "", 8)
|
||||
pdf.SetTextColor(149, 113, 22)
|
||||
pdf.SetXY(marginL+3, y+2)
|
||||
pdf.CellFormat(40, 4, "Sipariş Genel Açıklaması:", "", 0, "L", false, 0, "")
|
||||
|
||||
// Metin
|
||||
pdf.SetFont("dejavu", "", 8)
|
||||
pdf.SetTextColor(30, 30, 30)
|
||||
pdf.SetXY(marginL+48, y+2)
|
||||
pdf.MultiCell(textW, lineH, text, "", "L", false)
|
||||
|
||||
y += descBoxH + 3
|
||||
}
|
||||
|
||||
return y
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
GRID HEADER — 2 katmanlı + 16 beden kolonlu
|
||||
=========================================================== */
|
||||
|
||||
/*
|
||||
===========================================================
|
||||
|
||||
GRID HEADER — AÇIKLAMA içinde kategori listesi + 16 beden kolonu
|
||||
===========================================================
|
||||
*/
|
||||
func drawGridHeader(pdf *gofpdf.Fpdf, layout pdfLayout, startY float64, catSizes CategorySizeMap) float64 {
|
||||
pdf.SetFont("dejavu-b", "", 6)
|
||||
pdf.SetDrawColor(baggiGrayBorderR, baggiGrayBorderG, baggiGrayBorderB)
|
||||
pdf.SetFillColor(baggiCreamR, baggiCreamG, baggiCreamB)
|
||||
pdf.SetTextColor(20, 20, 20) // 🟣 TÜM HEADER YAZILARI SİYAH
|
||||
|
||||
y := startY
|
||||
x := layout.MarginL
|
||||
|
||||
totalHeaderH := float64(len(categoryOrder)) * layout.HeaderSizeH
|
||||
|
||||
centerLabel := func(h float64) float64 {
|
||||
return y + (h/2.0 - 3)
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------
|
||||
SOL BLOK (Model – Renk – Grup1 – Grup2)
|
||||
---------------------------------------------------- */
|
||||
cols := []struct {
|
||||
w float64
|
||||
t string
|
||||
}{
|
||||
{layout.ColModelW, "MODEL"},
|
||||
{layout.ColRenkW, "RENK"},
|
||||
{layout.ColGroupW, "ÜRÜN ANA"},
|
||||
{layout.ColGroupW2, "ÜRÜN ALT"},
|
||||
}
|
||||
|
||||
for _, c := range cols {
|
||||
pdf.Rect(x, y, c.w, totalHeaderH, "DF")
|
||||
pdf.SetXY(x, centerLabel(totalHeaderH))
|
||||
pdf.CellFormat(c.w, 6, c.t, "", 0, "C", false, 0, "")
|
||||
x += c.w
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------
|
||||
AÇIKLAMA BAŞLIĞI — TEK HÜCRE & DİKEY ORTALAMA
|
||||
---------------------------------------------------- */
|
||||
descX := x
|
||||
descW := layout.ColDescLeft
|
||||
|
||||
pdf.Rect(descX, y, descW, totalHeaderH, "DF")
|
||||
|
||||
// AÇIKLAMA yazısı ortalanacak
|
||||
pdf.SetXY(descX, y+(totalHeaderH/2-3))
|
||||
pdf.CellFormat(descW, 6, "AÇIKLAMA", "", 0, "C", false, 0, "")
|
||||
|
||||
/* ----------------------------------------------------
|
||||
AÇIKLAMA sağında kategori listesi dikey şekilde
|
||||
---------------------------------------------------- */
|
||||
catX := descX + 1
|
||||
catY := y + 2
|
||||
|
||||
pdf.SetFont("dejavu", "", 6.2)
|
||||
for _, cat := range categoryOrder {
|
||||
label := categoryTitle[cat]
|
||||
pdf.SetXY(catX+2, catY)
|
||||
pdf.CellFormat(descW-4, 4.8, label, "", 0, "L", false, 0, "")
|
||||
catY += layout.HeaderSizeH
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------
|
||||
16’lı BEDEN BLOĞU
|
||||
---------------------------------------------------- */
|
||||
x = descX + descW
|
||||
colW := layout.CentralW / 16.0
|
||||
|
||||
cy := y
|
||||
|
||||
for _, cat := range categoryOrder {
|
||||
|
||||
// Arka plan
|
||||
pdf.SetFillColor(baggiCreamR, baggiCreamG, baggiCreamB)
|
||||
pdf.Rect(x, cy, layout.CentralW, layout.HeaderSizeH, "DF")
|
||||
|
||||
sizes := catSizes[cat]
|
||||
if len(sizes) == 0 {
|
||||
sizes = defaultSizeListFor(cat)
|
||||
}
|
||||
if cat == catAksbir {
|
||||
pdf.SetFont("dejavu", "", 5) // sadece aksesuar için küçük font
|
||||
} else {
|
||||
pdf.SetFont("dejavu", "", 6) // diğer tüm kategoriler normal font
|
||||
}
|
||||
|
||||
xx := x
|
||||
for i := 0; i < 16; i++ {
|
||||
pdf.Rect(xx, cy, colW, layout.HeaderSizeH, "")
|
||||
if i < len(sizes) {
|
||||
pdf.SetXY(xx, cy+1)
|
||||
pdf.CellFormat(colW, layout.HeaderSizeH-2, sizes[i], "", 0, "C", false, 0, "")
|
||||
}
|
||||
xx += colW
|
||||
}
|
||||
|
||||
cy += layout.HeaderSizeH
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------
|
||||
SAĞ BLOK (ADET – FİYAT – PB – TUTAR – TERMİN)
|
||||
---------------------------------------------------- */
|
||||
rightX := x + 16*colW
|
||||
|
||||
rightCols := []struct {
|
||||
w float64
|
||||
t string
|
||||
}{
|
||||
{layout.ColQtyW, "ADET"},
|
||||
{layout.ColPriceW, "FİYAT"},
|
||||
{layout.ColCurW, "PB"},
|
||||
{layout.ColAmountW, "TUTAR"},
|
||||
{layout.ColTerminW, "TERMİN"},
|
||||
}
|
||||
|
||||
for _, c := range rightCols {
|
||||
pdf.Rect(rightX, y, c.w, totalHeaderH, "DF")
|
||||
pdf.SetXY(rightX, centerLabel(totalHeaderH))
|
||||
pdf.CellFormat(c.w, 6, c.t, "", 0, "C", false, 0, "")
|
||||
rightX += c.w
|
||||
}
|
||||
|
||||
return y + totalHeaderH
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
AÇIKLAMA WRAP + DİNAMİK SATIR YÜKSEKLİĞİ
|
||||
=========================================================== */
|
||||
|
||||
// Açıklama kolonunu çok satırlı yazan helper
|
||||
func drawWrappedCell(pdf *gofpdf.Fpdf, text string, x, y, w, h float64) {
|
||||
txt := strings.TrimSpace(text)
|
||||
if txt == "" {
|
||||
return
|
||||
}
|
||||
|
||||
lineH := 3.2
|
||||
lines := pdf.SplitLines([]byte(txt), w-2*OcellPadX)
|
||||
cy := y + 1
|
||||
|
||||
for _, ln := range lines {
|
||||
if cy+lineH > y+h {
|
||||
break
|
||||
}
|
||||
pdf.SetXY(x+OcellPadX, cy)
|
||||
pdf.CellFormat(w-2*OcellPadX, lineH, string(ln), "", 0, "L", false, 0, "")
|
||||
cy += lineH
|
||||
}
|
||||
}
|
||||
|
||||
// Açıklamaya göre satır yüksekliği hesaplar
|
||||
func calcRowHeight(pdf *gofpdf.Fpdf, layout pdfLayout, row PdfRow) float64 {
|
||||
base := layout.RowH
|
||||
desc := strings.TrimSpace(row.Description)
|
||||
if desc == "" {
|
||||
return base
|
||||
}
|
||||
|
||||
// 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())
|
||||
})
|
||||
}
|
||||
45
svc/routes/order_validate.go
Normal file
45
svc/routes/order_validate.go
Normal 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)
|
||||
}
|
||||
}
|
||||
89
svc/routes/orderinventory.go
Normal file
89
svc/routes/orderinventory.go
Normal 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
126
svc/routes/orderlist.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
131
svc/routes/orderpricelistb2b.go
Normal file
131
svc/routes/orderpricelistb2b.go
Normal 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 log’lu)
|
||||
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
239
svc/routes/orders.go
Normal 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,
|
||||
})
|
||||
})
|
||||
}
|
||||
116
svc/routes/password_forgot.go
Normal file
116
svc/routes/password_forgot.go
Normal 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️⃣ DB’ye 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,
|
||||
})
|
||||
}
|
||||
126
svc/routes/password_reset.go
Normal file
126
svc/routes/password_reset.go
Normal 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(¤tHash)
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
152
svc/routes/password_reset_complete.go
Normal file
152
svc/routes/password_reset_complete.go
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
63
svc/routes/password_reset_validate.go
Normal file
63
svc/routes/password_reset_validate.go
Normal 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}`))
|
||||
}
|
||||
}
|
||||
63
svc/routes/permission_debug.go
Normal file
63
svc/routes/permission_debug.go
Normal 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)
|
||||
}
|
||||
}
|
||||
203
svc/routes/permission_matrix_v2.go
Normal file
203
svc/routes/permission_matrix_v2.go
Normal 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
114
svc/routes/permissions.go
Normal 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
27
svc/routes/product.go
Normal 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)
|
||||
}
|
||||
48
svc/routes/productcolor.go
Normal file
48
svc/routes/productcolor.go
Normal 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))
|
||||
}
|
||||
73
svc/routes/productcolorsize.go
Normal file
73
svc/routes/productcolorsize.go
Normal 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)
|
||||
|
||||
// ✅ MSSQL’e 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))
|
||||
}
|
||||
65
svc/routes/productdetail.go
Normal file
65
svc/routes/productdetail.go
Normal 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)
|
||||
}
|
||||
67
svc/routes/productsecondcolor.go
Normal file
67
svc/routes/productsecondcolor.go
Normal 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))
|
||||
}
|
||||
440
svc/routes/role_department_permissions.go
Normal file
440
svc/routes/role_department_permissions.go
Normal 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)
|
||||
|
||||
// JWT’den 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)
|
||||
}
|
||||
}
|
||||
38
svc/routes/statement_detail.go
Normal file
38
svc/routes/statement_detail.go
Normal 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)
|
||||
}
|
||||
}
|
||||
38
svc/routes/statement_header.go
Normal file
38
svc/routes/statement_header.go
Normal 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)
|
||||
}
|
||||
}
|
||||
395
svc/routes/statement_header_pdf.go
Normal file
395
svc/routes/statement_header_pdf.go
Normal 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))
|
||||
}
|
||||
}
|
||||
642
svc/routes/statements_pdf.go
Normal file
642
svc/routes/statements_pdf.go
Normal 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} // secondary’den 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ı
|
||||
1–2 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
49
svc/routes/test_mail.go
Normal 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",
|
||||
})
|
||||
})
|
||||
}
|
||||
30
svc/routes/todaycurrencyv3.go
Normal file
30
svc/routes/todaycurrencyv3.go
Normal 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
320
svc/routes/user_detail.go
Normal 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
81
svc/routes/user_list.go
Normal 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)
|
||||
})
|
||||
}
|
||||
232
svc/routes/userpermissions.go
Normal file
232
svc/routes/userpermissions.go
Normal 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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user