Compare commits

..

27 Commits

Author SHA1 Message Date
M_Kececi
ac55f5a96c Merge remote-tracking branch 'origin/master' 2026-02-20 17:47:28 +03:00
M_Kececi
66df9b0f10 Merge remote-tracking branch 'origin/master' 2026-02-20 16:23:34 +03:00
M_Kececi
b4acdf3f60 Merge remote-tracking branch 'origin/master' 2026-02-20 16:10:59 +03:00
M_Kececi
a93630df7a Merge remote-tracking branch 'origin/master' 2026-02-20 16:06:40 +03:00
M_Kececi
47ca23f970 Merge remote-tracking branch 'origin/master' 2026-02-20 15:58:47 +03:00
M_Kececi
4ca8abb52f Merge remote-tracking branch 'origin/master' 2026-02-20 15:47:35 +03:00
M_Kececi
50f87e3290 Merge remote-tracking branch 'origin/master' 2026-02-20 15:22:58 +03:00
M_Kececi
d4583c17d2 Merge remote-tracking branch 'origin/master' 2026-02-20 12:36:04 +03:00
M_Kececi
32d0c38ab9 Merge remote-tracking branch 'origin/master' 2026-02-20 11:21:05 +03:00
M_Kececi
d9c527d13f Merge remote-tracking branch 'origin/master' 2026-02-20 08:52:06 +03:00
M_Kececi
f6b9793c41 Merge remote-tracking branch 'origin/master' 2026-02-20 08:50:05 +03:00
M_Kececi
1ced1b1649 Merge remote-tracking branch 'origin/master' 2026-02-19 12:28:13 +03:00
M_Kececi
76e7ca2e4a Merge remote-tracking branch 'origin/master' 2026-02-19 09:32:26 +03:00
M_Kececi
ed81fdf84f Merge remote-tracking branch 'origin/master' 2026-02-19 09:03:21 +03:00
M_Kececi
026c40c0b3 Merge remote-tracking branch 'origin/master' 2026-02-19 02:00:49 +03:00
M_Kececi
0136e6638b Merge remote-tracking branch 'origin/master' 2026-02-19 01:34:56 +03:00
M_Kececi
7184a40dd3 Merge remote-tracking branch 'origin/master' 2026-02-18 17:35:15 +03:00
M_Kececi
de58ef1043 Merge remote-tracking branch 'origin/master' 2026-02-18 16:58:46 +03:00
M_Kececi
744e20591d Merge remote-tracking branch 'origin/master' 2026-02-18 16:40:55 +03:00
M_Kececi
1263531edd Merge remote-tracking branch 'origin/master' 2026-02-18 16:40:37 +03:00
M_Kececi
d2bd0684c1 Merge remote-tracking branch 'origin/master' 2026-02-18 15:44:51 +03:00
M_Kececi
13f8801379 Merge remote-tracking branch 'origin/master' 2026-02-18 15:17:46 +03:00
M_Kececi
c3a1627152 Merge remote-tracking branch 'origin/master' 2026-02-18 15:09:47 +03:00
M_Kececi
727069910d Merge remote-tracking branch 'origin/master' 2026-02-18 14:34:21 +03:00
M_Kececi
1f95099677 Merge remote-tracking branch 'origin/master' 2026-02-18 14:24:50 +03:00
M_Kececi
dc36699a2b Merge remote-tracking branch 'origin/master' 2026-02-18 14:11:37 +03:00
M_Kececi
0e63370810 Merge remote-tracking branch 'origin/master' 2026-02-18 14:07:26 +03:00
91 changed files with 4261 additions and 1332 deletions

View File

@@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
set -euo pipefail set -euo pipefail
umask 022
export NODE_OPTIONS="--max_old_space_size=4096" export NODE_OPTIONS="--max_old_space_size=4096"
export CI="true" export CI="true"
@@ -20,13 +21,16 @@ RUNTIME_PRESERVE_FILES=(
"svc/public" "svc/public"
) )
log_step() {
echo "== $1 =="
}
backup_runtime_files() { backup_runtime_files() {
RUNTIME_BACKUP_DIR="$(mktemp -d /tmp/bssapp-runtime.XXXXXX)" RUNTIME_BACKUP_DIR="$(mktemp -d /tmp/bssapp-runtime.XXXXXX)"
for rel in "${RUNTIME_PRESERVE_FILES[@]}"; do for rel in "${RUNTIME_PRESERVE_FILES[@]}"; do
src="$APP_DIR/$rel" src="$APP_DIR/$rel"
dst="$RUNTIME_BACKUP_DIR/$rel" dst="$RUNTIME_BACKUP_DIR/$rel"
if [[ -e "$src" ]]; then if [[ -e "$src" ]]; then
mkdir -p "$(dirname "$dst")" mkdir -p "$(dirname "$dst")"
cp -a "$src" "$dst" cp -a "$src" "$dst"
@@ -47,8 +51,6 @@ cleanup_runtime_backup() {
} }
ensure_runtime_env_files() { ensure_runtime_env_files() {
# Bazı unit dosyaları EnvironmentFile olarak bu path'leri bekliyor.
# Dosyalar yoksa systemd "Failed to load environment files" ile düşüyor.
[[ -f "$APP_DIR/.env" ]] || touch "$APP_DIR/.env" [[ -f "$APP_DIR/.env" ]] || touch "$APP_DIR/.env"
[[ -f "$APP_DIR/mail.env" ]] || touch "$APP_DIR/mail.env" [[ -f "$APP_DIR/mail.env" ]] || touch "$APP_DIR/mail.env"
[[ -f "$APP_DIR/svc/.env" ]] || touch "$APP_DIR/svc/.env" [[ -f "$APP_DIR/svc/.env" ]] || touch "$APP_DIR/svc/.env"
@@ -56,8 +58,8 @@ ensure_runtime_env_files() {
} }
ensure_pdf_fonts() { ensure_pdf_fonts() {
font_dir="$APP_DIR/svc/fonts" local font_dir="$APP_DIR/svc/fonts"
sys_font_dir="/usr/share/fonts/truetype/dejavu" local sys_font_dir="/usr/share/fonts/truetype/dejavu"
mkdir -p "$font_dir" mkdir -p "$font_dir"
@@ -69,14 +71,44 @@ ensure_pdf_fonts() {
fi fi
if [[ ! -f "$font_dir/DejaVuSans.ttf" || ! -f "$font_dir/DejaVuSans-Bold.ttf" ]]; then if [[ ! -f "$font_dir/DejaVuSans.ttf" || ! -f "$font_dir/DejaVuSans-Bold.ttf" ]]; then
echo "ERROR: Required PDF fonts missing in $font_dir (DejaVuSans.ttf / DejaVuSans-Bold.ttf)" echo "ERROR: Required PDF fonts missing in $font_dir"
return 1 return 1
fi fi
} }
ensure_ui_permissions() {
local ui_root="$APP_DIR/ui/dist/spa"
if [[ ! -d "$ui_root" ]]; then
echo "ERROR: UI build output not found at $ui_root"
return 1
fi
chmod 755 /opt "$APP_DIR" "$APP_DIR/ui" "$APP_DIR/ui/dist" "$ui_root"
find "$ui_root" -type d -exec chmod 755 {} \;
find "$ui_root" -type f -exec chmod 644 {} \;
}
ensure_ui_readable_by_nginx() {
local ui_index="$APP_DIR/ui/dist/spa/index.html"
if [[ ! -f "$ui_index" ]]; then
echo "ERROR: UI index not found at $ui_index"
return 1
fi
if id -u www-data >/dev/null 2>&1; then
if ! su -s /bin/sh -c "test -r '$ui_index'" www-data; then
echo "ERROR: www-data cannot read $ui_index"
namei -l "$ui_index" || true
return 1
fi
fi
}
build_api_binary() { build_api_binary() {
if ! command -v go >/dev/null 2>&1; then if ! command -v go >/dev/null 2>&1; then
echo "go command not found; cannot build backend binary." echo "ERROR: go command not found"
return 1 return 1
fi fi
@@ -90,6 +122,26 @@ build_api_binary() {
chmod +x "$APP_DIR/svc/bssapp" chmod +x "$APP_DIR/svc/bssapp"
} }
restart_services() {
systemctl daemon-reload || true
systemctl restart bssapp
if ! systemctl is-active --quiet bssapp; then
echo "ERROR: bssapp service failed to start"
return 1
fi
if systemctl cat nginx >/dev/null 2>&1; then
systemctl restart nginx
if ! systemctl is-active --quiet nginx; then
echo "ERROR: nginx service failed to start"
return 1
fi
else
echo "WARN: nginx service not found; frontend may be unreachable."
fi
}
run_deploy() { run_deploy() {
trap cleanup_runtime_backup EXIT trap cleanup_runtime_backup EXIT
@@ -105,7 +157,7 @@ run_deploy() {
cd "$APP_DIR" cd "$APP_DIR"
echo "== GIT SYNC ==" log_step "GIT SYNC"
backup_runtime_files backup_runtime_files
git fetch origin git fetch origin
git reset --hard origin/master git reset --hard origin/master
@@ -118,37 +170,43 @@ run_deploy() {
-e svc/public \ -e svc/public \
-e svc/bssapp -e svc/bssapp
restore_runtime_files restore_runtime_files
echo "DEPLOY COMMIT: $(git rev-parse --short HEAD)"
echo "== BUILD UI ==" log_step "BUILD UI"
cd "$APP_DIR/ui" cd "$APP_DIR/ui"
npm ci --no-audit --no-fund --include=optional npm ci --no-audit --no-fund --include=optional
# Linux'ta sass --embedded hatasını engellemek için
# deploy sırasında çalışan node_modules ağacına doğrudan yazıyoruz.
npm i -D --no-audit --no-fund sass-embedded@1.93.2 npm i -D --no-audit --no-fund sass-embedded@1.93.2
npm run build npm run build
echo "== BUILD API ==" log_step "ENSURE UI PERMISSIONS"
ensure_ui_permissions
ensure_ui_readable_by_nginx
log_step "BUILD API"
build_api_binary build_api_binary
echo "== ENSURE ENV FILES ==" log_step "ENSURE ENV FILES"
ensure_runtime_env_files ensure_runtime_env_files
echo "== ENSURE PDF FONTS ==" log_step "ENSURE PDF FONTS"
ensure_pdf_fonts ensure_pdf_fonts
echo "== RESTART SERVICE ==" log_step "RESTART SERVICES"
systemctl restart bssapp restart_services
echo "[DEPLOY FINISHED] $(date '+%F %T')" echo "[DEPLOY FINISHED] $(date '+%F %T')"
} }
if [[ "${1:-}" == "--run" ]]; then if [[ "${1:-}" == "--run" ]]; then
mkdir -p "$(dirname "$LOG_FILE")"
if command -v logger >/dev/null 2>&1; then
run_deploy 2>&1 | tee -a "$LOG_FILE" >(logger -t bssapp-deploy -p user.info)
exit ${PIPESTATUS[0]}
else
run_deploy >>"$LOG_FILE" 2>&1 run_deploy >>"$LOG_FILE" 2>&1
exit 0 exit $?
fi
fi fi
# Fully detach webhook-triggered process to avoid EPIPE from closed request sockets.
nohup /bin/bash "$0" --run </dev/null >/dev/null 2>&1 & nohup /bin/bash "$0" --run </dev/null >/dev/null 2>&1 &
exit 0 exit 0

View File

@@ -10,6 +10,38 @@
} }
], ],
"trigger-rule": { "trigger-rule": {
"or": [
{
"match": {
"type": "value",
"value": "Bearer bssapp-secret-2026",
"parameter": {
"source": "header",
"name": "Authorization"
}
}
},
{
"match": {
"type": "value",
"value": "bssapp-secret-2026",
"parameter": {
"source": "header",
"name": "Authorization"
}
}
},
{
"match": {
"type": "value",
"value": "X-BSSAPP-SECRET: bssapp-secret-2026",
"parameter": {
"source": "header",
"name": "Authorization"
}
}
},
{
"match": { "match": {
"type": "value", "type": "value",
"value": "bssapp-secret-2026", "value": "bssapp-secret-2026",
@@ -19,5 +51,7 @@
} }
} }
} }
]
}
} }
] ]

View File

@@ -0,0 +1,27 @@
/* Indexes for order validate performance */
IF NOT EXISTS (
SELECT 1
FROM sys.indexes
WHERE name = 'IX_trOrderLine_OrderHeader_ItemCode'
AND object_id = OBJECT_ID('dbo.trOrderLine')
)
BEGIN
CREATE NONCLUSTERED INDEX IX_trOrderLine_OrderHeader_ItemCode
ON dbo.trOrderLine (OrderHeaderID, ItemCode)
INCLUDE (ItemTypeCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, LineDescription, SortOrder, OrderLineID);
END
GO
IF NOT EXISTS (
SELECT 1
FROM sys.indexes
WHERE name = 'IX_prItemVariant_Combo'
AND object_id = OBJECT_ID('dbo.prItemVariant')
)
BEGIN
CREATE NONCLUSTERED INDEX IX_prItemVariant_Combo
ON dbo.prItemVariant (ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code)
INCLUDE (PLU);
END
GO

33
svc/.env.local Normal file
View File

@@ -0,0 +1,33 @@
# ===============================
# SECURITY
# ===============================
JWT_SECRET=bssapp_super_secret_key_1234567890
PASSWORD_RESET_SECRET=1dc7d6d52fd0459a8b1f288a6590428e760f54339f8e47beb20db36b6df6070b
# ===============================
# URLS (PRODUCTION)
# ===============================
APP_FRONTEND_URL=http://ss.baggi.com.tr/app
API_URL=http://46.224.33.150
# Eğer Nginx ile /api varsa:
# API_URL=http://46.224.33.150/api
# ===============================
# UI
# ===============================
UI_DIR=/opt/bssapp/ui/dist
# ===============================
# DATABASES
# ===============================
POSTGRES_CONN=host=46.224.33.150 port=5432 user=postgres password=tayitkan dbname=baggib2b sslmode=disable
MSSQL_CONN=sqlserver://sa:Gil_0150@100.127.186.137:1433?database=BAGGI_V3&encrypt=disable
# ===============================
# PDF
# ===============================
PDF_FONT_DIR=/opt/bssapp/svc/fonts
API_HOST=0.0.0.0
API_PORT=8080

View File

@@ -12,12 +12,11 @@ import (
var MssqlDB *sql.DB var MssqlDB *sql.DB
// ConnectMSSQL MSSQL baglantisini ortam degiskeninden baslatir.
func ConnectMSSQL() { func ConnectMSSQL() {
connString := strings.TrimSpace(os.Getenv("MSSQL_CONN")) connString := strings.TrimSpace(os.Getenv("MSSQL_CONN"))
if connString == "" { if connString == "" {
// Fallback log.Fatal("MSSQL_CONN tanımlı değil")
connString = "server=100.127.186.137;user id=sa;password=Gi l_0150;port=1433;database=BAGGI_V3"
} }
var err error var err error
@@ -26,12 +25,11 @@ func ConnectMSSQL() {
log.Fatal("MSSQL bağlantı hatası:", err) log.Fatal("MSSQL bağlantı hatası:", err)
} }
err = MssqlDB.Ping() if err = MssqlDB.Ping(); err != nil {
if err != nil {
log.Fatal("MSSQL erişilemiyor:", err) log.Fatal("MSSQL erişilemiyor:", err)
} }
fmt.Println("MSSQL bağlantısı başarılı!") fmt.Println("MSSQL bağlantısı başarılı")
} }
func GetDB() *sql.DB { func GetDB() *sql.DB {

View File

@@ -13,14 +13,11 @@ import (
var PgDB *sql.DB var PgDB *sql.DB
// ConnectPostgres PostgreSQL veritabanına bağlanır // ConnectPostgres PostgreSQL veritabanına bağlanır.
func ConnectPostgres() (*sql.DB, error) { func ConnectPostgres() (*sql.DB, error) {
// Bağlantı stringi (istersen .envden oku) connStr := strings.TrimSpace(os.Getenv("POSTGRES_CONN"))
connStr := os.Getenv("POSTGRES_CONN")
if connStr == "" { if connStr == "" {
// fallback → sabit tanımlı bağlantı return nil, fmt.Errorf("POSTGRES_CONN tanımlı değil")
connStr = "host= 46.224.33.150 port=5432 user=postgres password=tayitkan dbname=baggib2b sslmode=disable"
//connStr = "host=172.16.0.3 port=5432 user=postgres password=tayitkan dbname=baggib2b sslmode=disable"
} }
db, err := sql.Open("postgres", connStr) db, err := sql.Open("postgres", connStr)
@@ -28,15 +25,13 @@ func ConnectPostgres() (*sql.DB, error) {
return nil, fmt.Errorf("PostgreSQL bağlantı hatası: %w", err) return nil, fmt.Errorf("PostgreSQL bağlantı hatası: %w", err)
} }
// ======================================================= // Bağlantı havuzu ayarları (audit log uyumlu).
// 🔹 BAĞLANTI HAVUZU (AUDIT LOG UYUMLU) db.SetMaxOpenConns(30)
// =======================================================
db.SetMaxOpenConns(30) // audit + api paralel çalışsın
db.SetMaxIdleConns(10) db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(30 * time.Minute) db.SetConnMaxLifetime(30 * time.Minute)
db.SetConnMaxIdleTime(5 * time.Minute) // 🔥 uzun idle audit bağlantılarını kapat db.SetConnMaxIdleTime(5 * time.Minute)
// 🔹 Test et // Bağlantıyı test et.
if err = db.Ping(); err != nil { if err = db.Ping(); err != nil {
// Some managed PostgreSQL servers require TLS. If the current DSN uses // Some managed PostgreSQL servers require TLS. If the current DSN uses
// sslmode=disable and server rejects with "no encryption", retry once // sslmode=disable and server rejects with "no encryption", retry once
@@ -45,7 +40,7 @@ func ConnectPostgres() (*sql.DB, error) {
strings.Contains(err.Error(), "no encryption") && strings.Contains(err.Error(), "no encryption") &&
strings.Contains(strings.ToLower(connStr), "sslmode=disable") { strings.Contains(strings.ToLower(connStr), "sslmode=disable") {
secureConnStr := strings.Replace(connStr, "sslmode=disable", "sslmode=require", 1) secureConnStr := strings.Replace(connStr, "sslmode=disable", "sslmode=require", 1)
log.Println("⚠️ PostgreSQL requires TLS, retrying with sslmode=require") log.Println("PostgreSQL TLS gerektiriyor, sslmode=require ile tekrar deneniyor")
_ = db.Close() _ = db.Close()
db, err = sql.Open("postgres", secureConnStr) db, err = sql.Open("postgres", secureConnStr)
@@ -66,13 +61,12 @@ func ConnectPostgres() (*sql.DB, error) {
} }
} }
log.Println("PostgreSQL bağlantısı başarılı!") log.Println("PostgreSQL bağlantısı başarılı")
PgDB = db PgDB = db
return db, nil return db, nil
} }
// GetPostgresUsers test amaçlı ilk 5 kullanıcıyı listeler // GetPostgresUsers test amaçlı ilk 5 kullanıcıyı listeler.
func GetPostgresUsers(db *sql.DB) error { func GetPostgresUsers(db *sql.DB) error {
query := `SELECT id, code, email FROM mk_dfusr ORDER BY id LIMIT 5` query := `SELECT id, code, email FROM mk_dfusr ORDER BY id LIMIT 5`
rows, err := db.Query(query) rows, err := db.Query(query)
@@ -81,14 +75,14 @@ func GetPostgresUsers(db *sql.DB) error {
} }
defer rows.Close() defer rows.Close()
fmt.Println("📋 İlk 5 PostgreSQL kullanıcısı:") fmt.Println("İlk 5 PostgreSQL kullanıcısı:")
for rows.Next() { for rows.Next() {
var id int var id int
var code, email string var code, email string
if err := rows.Scan(&id, &code, &email); err != nil { if err := rows.Scan(&id, &code, &email); err != nil {
return err return err
} }
fmt.Printf(" ID: %-4d | USER: %-20s | EMAIL: %s\n", id, code, email) fmt.Printf(" -> ID: %-4d | USER: %-20s | EMAIL: %s\n", id, code, email)
} }
return rows.Err() return rows.Err()

View File

@@ -9,8 +9,9 @@ func (m *GraphMailer) SendPasswordResetMail(toEmail string, resetURL string) err
<p>Merhaba,</p> <p>Merhaba,</p>
<p>Parolanızı sıfırlamak için aşağıdaki bağlantıya tıklayın:</p> <p>Parolanızı sıfırlamak için aşağıdaki bağlantıya tıklayın:</p>
<p> <p>
<a href="%s">%s</a> <a href="%s">Parolayı sıfırla</a>
</p> </p>
<p style="font-size:12px;color:#666;">Bağlantı: %s</p>
<p>Bu bağlantı <strong>30 dakika</strong> geçerlidir ve tek kullanımlıktır.</p> <p>Bu bağlantı <strong>30 dakika</strong> geçerlidir ve tek kullanımlıktır.</p>
`, resetURL, resetURL) `, resetURL, resetURL)

View File

@@ -2,12 +2,28 @@ package security
import ( import (
"os" "os"
"strings"
) )
func BuildResetURL(token string) string { func BuildResetURL(token string) string {
base := os.Getenv("FRONTEND_URL") base := os.Getenv("APP_FRONTEND_URL")
if base == "" {
base = os.Getenv("FRONTEND_URL")
}
if base == "" { if base == "" {
base = "http://localhost:9000" base = "http://localhost:9000"
} }
base = strings.TrimRight(base, "/")
// If base already points to password-reset, just append token if needed.
if strings.Contains(base, "/password-reset") {
if strings.HasSuffix(base, "/password-reset") || strings.HasSuffix(base, "/password-reset/") ||
strings.HasSuffix(base, "/#/password-reset") || strings.HasSuffix(base, "/#/password-reset/") {
return strings.TrimRight(base, "/") + "/" + token
}
return base
}
if strings.Contains(base, "#") {
return base + "/password-reset/" + token return base + "/password-reset/" + token
} }
return base + "/#/password-reset/" + token
}

View File

@@ -207,24 +207,41 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
routes.AuthRefreshHandler(pgDB), routes.AuthRefreshHandler(pgDB),
) )
// Password reset flow (public)
bindV3(r, pgDB,
"/api/password/forgot", "POST",
"auth", "update",
routes.ForgotPasswordHandler(pgDB, ml),
)
bindV3(r, pgDB,
"/api/password/reset/validate/{token}", "GET",
"auth", "view",
routes.ValidatePasswordResetTokenHandler(pgDB),
)
bindV3(r, pgDB,
"/api/password/reset", "POST",
"auth", "update",
routes.CompletePasswordResetHandler(pgDB),
)
// ============================================================ // ============================================================
// SYSTEM // SYSTEM
// ============================================================ // ============================================================
bindV3(r, pgDB, bindV3(r, pgDB,
"/api/password/change", "POST", "/api/password/change", "POST",
"system", "update", "auth", "update",
wrapV3(http.HandlerFunc(routes.FirstPasswordChangeHandler(pgDB))), wrapV3(http.HandlerFunc(routes.FirstPasswordChangeHandler(pgDB))),
) )
bindV3(r, pgDB, bindV3(r, pgDB,
"/api/activity-logs", "GET", "/api/activity-logs", "GET",
"user", "view", "system", "read",
wrapV3(routes.AdminActivityLogsHandler(pgDB)), wrapV3(routes.AdminActivityLogsHandler(pgDB)),
) )
bindV3(r, pgDB, bindV3(r, pgDB,
"/api/test-mail", "POST", "/api/test-mail", "POST",
"user", "insert", "system", "update",
wrapV3(routes.TestMailHandler(ml)), wrapV3(routes.TestMailHandler(ml)),
) )
@@ -235,12 +252,12 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
bindV3(r, pgDB, bindV3(r, pgDB,
rolePerm, "GET", rolePerm, "GET",
"user", "update", "system", "update",
wrapV3(routes.GetRolePermissionMatrix(pgDB)), wrapV3(routes.GetRolePermissionMatrix(pgDB)),
) )
bindV3(r, pgDB, bindV3(r, pgDB,
rolePerm, "POST", rolePerm, "POST",
"user", "update", "system", "update",
wrapV3(routes.SaveRolePermissionMatrix(pgDB)), wrapV3(routes.SaveRolePermissionMatrix(pgDB)),
) )
@@ -248,12 +265,12 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
bindV3(r, pgDB, bindV3(r, pgDB,
userPerm, "GET", userPerm, "GET",
"user", "update", "system", "update",
wrapV3(routes.GetUserPermissionsHandler(pgDB)), wrapV3(routes.GetUserPermissionsHandler(pgDB)),
) )
bindV3(r, pgDB, bindV3(r, pgDB,
userPerm, "POST", userPerm, "POST",
"user", "update", "system", "update",
wrapV3(routes.SaveUserPermissionsHandler(pgDB)), wrapV3(routes.SaveUserPermissionsHandler(pgDB)),
) )
@@ -286,17 +303,17 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
bindV3(r, pgDB, bindV3(r, pgDB,
"/api/role-dept-permissions/list", "GET", "/api/role-dept-permissions/list", "GET",
"user", "update", "system", "update",
wrapV3(http.HandlerFunc(rdHandler.List)), wrapV3(http.HandlerFunc(rdHandler.List)),
) )
bindV3(r, pgDB, bindV3(r, pgDB,
rdPerm, "GET", rdPerm, "GET",
"user", "update", "system", "update",
wrapV3(http.HandlerFunc(rdHandler.Get)), wrapV3(http.HandlerFunc(rdHandler.Get)),
) )
bindV3(r, pgDB, bindV3(r, pgDB,
rdPerm, "POST", rdPerm, "POST",
"user", "update", "system", "update",
wrapV3(http.HandlerFunc(rdHandler.Save)), wrapV3(http.HandlerFunc(rdHandler.Save)),
) )
@@ -439,6 +456,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
{"/api/order/update", "POST", "update", http.HandlerFunc(routes.UpdateOrderHandler)}, {"/api/order/update", "POST", "update", http.HandlerFunc(routes.UpdateOrderHandler)},
{"/api/order/get/{id}", "GET", "view", routes.GetOrderByIDHandler(mssql)}, {"/api/order/get/{id}", "GET", "view", routes.GetOrderByIDHandler(mssql)},
{"/api/orders/list", "GET", "view", routes.OrderListRoute(mssql)}, {"/api/orders/list", "GET", "view", routes.OrderListRoute(mssql)},
{"/api/orders/production-list", "GET", "update", routes.OrderProductionListRoute(mssql)},
{"/api/orders/production-items/{id}", "GET", "view", routes.OrderProductionItemsRoute(mssql)},
{"/api/orders/production-items/{id}/insert-missing", "POST", "update", routes.OrderProductionInsertMissingRoute(mssql)},
{"/api/orders/production-items/{id}/validate", "POST", "update", routes.OrderProductionValidateRoute(mssql)},
{"/api/orders/production-items/{id}/apply", "POST", "update", routes.OrderProductionApplyRoute(mssql)},
{"/api/orders/close-ready", "GET", "update", routes.OrderCloseReadyListRoute(mssql)}, {"/api/orders/close-ready", "GET", "update", routes.OrderCloseReadyListRoute(mssql)},
{"/api/orders/bulk-close", "POST", "update", routes.OrderBulkCloseRoute(mssql)}, {"/api/orders/bulk-close", "POST", "update", routes.OrderBulkCloseRoute(mssql)},
{"/api/orders/export", "GET", "export", routes.OrderListExcelRoute(mssql)}, {"/api/orders/export", "GET", "export", routes.OrderListExcelRoute(mssql)},
@@ -616,16 +638,21 @@ func main() {
), ),
) )
port := strings.TrimSpace(os.Getenv("HTTPPORT")) host := strings.TrimSpace(os.Getenv("API_HOST"))
port := strings.TrimSpace(os.Getenv("API_PORT"))
if host == "" {
host = "0.0.0.0"
}
if port == "" { if port == "" {
port = "8080" port = "8080"
} }
if !strings.HasPrefix(port, ":") {
port = ":" + port
}
log.Println("Server calisiyor: http://localhost" + port) addr := host + ":" + port
log.Fatal(http.ListenAndServe(port, handler))
log.Println("🚀 Server running at:", addr)
log.Fatal(http.ListenAndServe(addr, handler))
} }
func mountSPA(r *mux.Router) { func mountSPA(r *mux.Router) {
@@ -685,12 +712,17 @@ func uiRootDir() string {
return d return d
} }
candidates := []string{"../ui/dist", "./ui/dist"} candidates := []string{
"../ui/dist/spa",
"./ui/dist/spa",
"../ui/dist",
"./ui/dist",
}
for _, d := range candidates { for _, d := range candidates {
if fi, err := os.Stat(d); err == nil && fi.IsDir() { if fi, err := os.Stat(d); err == nil && fi.IsDir() {
return d return d
} }
} }
return "../ui/dist" return "../ui/dist/spa"
} }

View File

@@ -21,6 +21,7 @@ type OrderList struct {
// Sipariş Durumu // Sipariş Durumu
CreditableConfirmedDate string `json:"CreditableConfirmedDate"` CreditableConfirmedDate string `json:"CreditableConfirmedDate"`
IsCreditableConfirmed bool `json:"IsCreditableConfirmed"` IsCreditableConfirmed bool `json:"IsCreditableConfirmed"`
HasUretimUrunu bool `json:"HasUretimUrunu"`
// 💱 Para Birimi // 💱 Para Birimi
DocCurrencyCode string `json:"DocCurrencyCode"` DocCurrencyCode string `json:"DocCurrencyCode"`

View File

@@ -0,0 +1,25 @@
package models
// ========================================================
// 📌 OrderProductionItem — U ile başlayan ürün satırı
// ========================================================
type OrderProductionItem struct {
OrderHeaderID string `json:"OrderHeaderID"`
OrderLineID string `json:"OrderLineID"`
ItemTypeCode int16 `json:"ItemTypeCode"`
OldDim1 string `json:"OldDim1"`
OldDim3 string `json:"OldDim3"`
OldItemCode string `json:"OldItemCode"`
OldColor string `json:"OldColor"`
OldDim2 string `json:"OldDim2"`
OldDesc string `json:"OldDesc"`
NewItemCode string `json:"NewItemCode"`
NewColor string `json:"NewColor"`
NewDim2 string `json:"NewDim2"`
NewDesc string `json:"NewDesc"`
IsVariantMissing bool `json:"IsVariantMissing"`
}

View File

@@ -0,0 +1,24 @@
package models
type OrderProductionUpdateLine struct {
OrderLineID string `json:"OrderLineID"`
NewItemCode string `json:"NewItemCode"`
NewColor string `json:"NewColor"`
NewDim2 string `json:"NewDim2"`
NewDesc string `json:"NewDesc"`
}
type OrderProductionUpdatePayload struct {
Lines []OrderProductionUpdateLine `json:"lines"`
InsertMissing bool `json:"insertMissing"`
}
type OrderProductionMissingVariant struct {
OrderLineID string `json:"OrderLineID"`
ItemTypeCode int16 `json:"ItemTypeCode"`
ItemCode string `json:"ItemCode"`
ColorCode string `json:"ColorCode"`
ItemDim1Code string `json:"ItemDim1Code"`
ItemDim2Code string `json:"ItemDim2Code"`
ItemDim3Code string `json:"ItemDim3Code"`
}

View File

@@ -101,8 +101,8 @@ SELECT
L.ItemDim1Code, L.ItemDim1Code,
L.ItemDim2Code, L.ItemDim2Code,
L.Qty1, L.Qty1,
L.Price, ISNULL(CD.Price, 0) AS Price,
L.DocCurrencyCode, ISNULL(CD.CurrencyCode, ISNULL(L.DocCurrencyCode, 'TRY')) AS DocCurrencyCode,
L.DeliveryDate, L.DeliveryDate,
L.LineDescription, L.LineDescription,
P.ProductAtt01Desc, P.ProductAtt01Desc,
@@ -115,6 +115,9 @@ SELECT
L.VatCode, L.VatCode,
L.VatRate L.VatRate
FROM BAGGI_V3.dbo.trOrderLine AS L FROM BAGGI_V3.dbo.trOrderLine AS L
LEFT JOIN BAGGI_V3.dbo.trOrderLineCurrency AS CD WITH (NOLOCK)
ON CD.OrderLineID = L.OrderLineID
AND CD.CurrencyCode = ISNULL(NULLIF(LTRIM(RTRIM(L.DocCurrencyCode)), ''), 'TRY')
LEFT JOIN ProductFilterWithDescription('TR') AS P LEFT JOIN ProductFilterWithDescription('TR') AS P
ON LTRIM(RTRIM(P.ProductCode)) = LTRIM(RTRIM(L.ItemCode)) ON LTRIM(RTRIM(P.ProductCode)) = LTRIM(RTRIM(L.ItemCode))
WHERE L.OrderHeaderID = @p1 WHERE L.OrderHeaderID = @p1

View File

@@ -13,6 +13,7 @@ import (
"bssapp-backend/models" "bssapp-backend/models"
"database/sql" "database/sql"
"fmt" "fmt"
"math"
"strings" "strings"
"time" "time"
@@ -1114,8 +1115,9 @@ func UpdateOrder(header models.OrderHeader, lines []models.OrderDetail, user *mo
DELETE FROM BAGGI_V3.dbo.trOrderLine DELETE FROM BAGGI_V3.dbo.trOrderLine
WHERE OrderHeaderID=@p1 AND OrderLineID=@p2 AND ISNULL(IsClosed,0)=0 WHERE OrderHeaderID=@p1 AND OrderLineID=@p2 AND ISNULL(IsClosed,0)=0
`, header.OrderHeaderID, lineID); err != nil { `, header.OrderHeaderID, lineID); err != nil {
fmt.Printf("[ORDER_UPDATE] hard delete failed, trying soft-close line_id=%s err=%v\n", lineID, err) fmt.Printf("[ORDER_UPDATE] hard delete failed, trying qty-zero soft-close line_id=%s err=%v\n", lineID, err)
// IsClosed computed olabilir; sadece miktarları sıfırla.
if _, err2 := tx.Exec(` if _, err2 := tx.Exec(`
UPDATE BAGGI_V3.dbo.trOrderLine UPDATE BAGGI_V3.dbo.trOrderLine
SET SET
@@ -1123,12 +1125,11 @@ SET
Qty2 = 0, Qty2 = 0,
CancelQty1 = 0, CancelQty1 = 0,
CancelQty2 = 0, CancelQty2 = 0,
IsClosed = 1,
LastUpdatedUserName = @p1, LastUpdatedUserName = @p1,
LastUpdatedDate = @p2 LastUpdatedDate = @p2
WHERE OrderHeaderID=@p3 AND OrderLineID=@p4 AND ISNULL(IsClosed,0)=0 WHERE OrderHeaderID=@p3 AND OrderLineID=@p4 AND ISNULL(IsClosed,0)=0
`, v3User, now, header.OrderHeaderID, lineID); err2 != nil { `, v3User, now, header.OrderHeaderID, lineID); err2 != nil {
return fmt.Errorf("line delete failed line_id=%s: %v; soft-close failed: %w", lineID, err, err2) return fmt.Errorf("line delete failed line_id=%s: %v; qty-zero soft-close failed: %w", lineID, err, err2)
} }
} }
@@ -1155,6 +1156,13 @@ WHERE OrderHeaderID=@p3 AND OrderLineID=@p4 AND ISNULL(IsClosed,0)=0
color string color string
dim1 string dim1 string
dim2 string dim2 string
qty1 float64
price float64
docCurrency string
vatRate float64
lineDesc string
deliveryDate string
plannedDate string
}) })
rows, err := tx.Query(` rows, err := tx.Query(`
@@ -1164,7 +1172,14 @@ SELECT
ISNULL(ItemCode,''), ISNULL(ItemCode,''),
ISNULL(ColorCode,''), ISNULL(ColorCode,''),
ISNULL(ItemDim1Code,''), ISNULL(ItemDim1Code,''),
ISNULL(ItemDim2Code,'') ISNULL(ItemDim2Code,''),
ISNULL(Qty1, 0),
ISNULL(Price, 0),
ISNULL(DocCurrencyCode, ''),
ISNULL(VatRate, 0),
ISNULL(LineDescription, ''),
CONVERT(varchar(10), DeliveryDate, 23),
CONVERT(varchar(10), PlannedDateOfLading, 23)
FROM BAGGI_V3.dbo.trOrderLine FROM BAGGI_V3.dbo.trOrderLine
WHERE OrderHeaderID=@p1 WHERE OrderHeaderID=@p1
`, header.OrderHeaderID) `, header.OrderHeaderID)
@@ -1175,9 +1190,15 @@ WHERE OrderHeaderID=@p1
for rows.Next() { for rows.Next() {
var id, item, color, dim1, dim2 string var id, item, color, dim1, dim2 string
var docCurrency, lineDesc, deliveryDate, plannedDate string
var qty1, price, vatRate float64
var closed bool var closed bool
if err := rows.Scan(&id, &closed, &item, &color, &dim1, &dim2); err != nil { if err := rows.Scan(
&id, &closed, &item, &color, &dim1, &dim2,
&qty1, &price, &docCurrency, &vatRate, &lineDesc,
&deliveryDate, &plannedDate,
); err != nil {
return nil, err return nil, err
} }
@@ -1195,11 +1216,25 @@ WHERE OrderHeaderID=@p1
color string color string
dim1 string dim1 string
dim2 string dim2 string
qty1 float64
price float64
docCurrency string
vatRate float64
lineDesc string
deliveryDate string
plannedDate string
}{ }{
item: strings.TrimSpace(item), item: strings.TrimSpace(item),
color: strings.TrimSpace(color), color: strings.TrimSpace(color),
dim1: strings.TrimSpace(dim1), dim1: strings.TrimSpace(dim1),
dim2: strings.TrimSpace(dim2), dim2: strings.TrimSpace(dim2),
qty1: qty1,
price: price,
docCurrency: strings.TrimSpace(docCurrency),
vatRate: vatRate,
lineDesc: strings.TrimSpace(lineDesc),
deliveryDate: strings.TrimSpace(deliveryDate),
plannedDate: strings.TrimSpace(plannedDate),
} }
if combo != "" { if combo != "" {
existingOpenCombo[combo] = id existingOpenCombo[combo] = id
@@ -1391,6 +1426,46 @@ WHERE OrderLineID=@p42 AND ISNULL(IsClosed,0)=0`)
} }
} }
// Eğer satır DB'deki değerlerle aynıysa, update/upsert yapma (performans)
if !isNew && ln.OrderLineID != "" {
if meta, ok := existingOpenMeta[ln.OrderLineID]; ok {
trim := strings.TrimSpace
floatEq := func(a, b float64) bool { return math.Abs(a-b) < 0.0001 }
dateOnly := func(nt models.NullTime) string {
if nt.Valid && !nt.Time.IsZero() {
return nt.Time.Format("2006-01-02")
}
return ""
}
plannedStr := ""
if ln.PlannedDateOfLading.Valid {
plannedStr = strings.TrimSpace(ln.PlannedDateOfLading.String)
}
deliveryStr := dateOnly(ln.DeliveryDate)
payloadHasDates := deliveryStr != "" && plannedStr != ""
if payloadHasDates &&
trim(meta.item) == trim(safeNS(ln.ItemCode)) &&
trim(meta.color) == trim(safeNS(ln.ColorCode)) &&
trim(meta.dim1) == trim(safeNS(ln.ItemDim1Code)) &&
trim(meta.dim2) == trim(safeNS(ln.ItemDim2Code)) &&
floatEq(meta.qty1, nf0(ln.Qty1)) &&
floatEq(meta.price, nf0(ln.Price)) &&
trim(meta.docCurrency) == trim(safeNS(ln.DocCurrencyCode)) &&
floatEq(meta.vatRate, nf0(ln.VatRate)) &&
trim(meta.lineDesc) == trim(safeNS(ln.LineDescription)) &&
trim(meta.deliveryDate) == trim(deliveryStr) &&
trim(meta.plannedDate) == trim(plannedStr) {
// Bu satırı "işlendi" say, ama DB yazma yok
delete(existingOpen, ln.OrderLineID)
delete(existingOpenCombo, comboKey)
continue
}
}
}
// Variant guard // Variant guard
if qtyValue(ln.Qty1) > 0 { if qtyValue(ln.Qty1) > 0 {
if err := ValidateItemVariant(tx, ln); err != nil { if err := ValidateItemVariant(tx, ln); err != nil {

View File

@@ -112,6 +112,16 @@ SELECT
END AS PackedRatePct, END AS PackedRatePct,
ISNULL(h.IsCreditableConfirmed,0) AS IsCreditableConfirmed, ISNULL(h.IsCreditableConfirmed,0) AS IsCreditableConfirmed,
CASE
WHEN EXISTS (
SELECT 1
FROM dbo.trOrderLine l2
WHERE l2.OrderHeaderID = h.OrderHeaderID
AND ISNULL(l2.ItemCode,'') LIKE 'U%%'
)
THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS HasUretimUrunu,
ISNULL(h.Description,'') AS Description, ISNULL(h.Description,'') AS Description,
usd.Rate AS ExchangeRateUSD usd.Rate AS ExchangeRateUSD

View File

@@ -0,0 +1,222 @@
package queries
import (
"database/sql"
"bssapp-backend/models"
)
// ========================================================
// 📌 GetOrderProductionItems — OrderHeaderID için U ürünleri
// ========================================================
func GetOrderProductionItems(mssql *sql.DB, orderHeaderID string) (*sql.Rows, error) {
return mssql.Query(`
SELECT
CAST(l.OrderHeaderID AS NVARCHAR(50)) AS OrderHeaderID,
CAST(l.OrderLineID AS NVARCHAR(50)) AS OrderLineID,
l.ItemTypeCode AS ItemTypeCode,
ISNULL(l.ItemDim1Code,'') AS OldDim1,
ISNULL(l.ItemDim3Code,'') AS OldDim3,
ISNULL(l.ItemCode,'') AS OldItemCode,
ISNULL(l.ColorCode,'') AS OldColor,
ISNULL(l.ItemDim2Code,'') AS OldDim2,
ISNULL(l.LineDescription,'') AS OldDesc,
CAST('' AS NVARCHAR(60)) AS NewItemCode,
CAST('' AS NVARCHAR(30)) AS NewColor,
CAST('' AS NVARCHAR(30)) AS NewDim2,
CAST('' AS NVARCHAR(250)) AS NewDesc,
CAST(0 AS bit) AS IsVariantMissing
FROM dbo.trOrderLine l
WHERE l.OrderHeaderID = @p1
AND ISNULL(l.ItemCode,'') LIKE 'U%'
ORDER BY l.SortOrder, l.OrderLineID
`, orderHeaderID)
}
// ========================================================
// 📌 InsertMissingProductionVariants — eksik prItemVariant ekler
// ========================================================
func InsertMissingProductionVariants(mssql *sql.DB, orderHeaderID string, username string) (int64, error) {
query := `
;WITH Missing AS (
SELECT DISTINCT
l.ItemTypeCode,
l.ItemCode,
l.ColorCode,
l.ItemDim1Code,
l.ItemDim2Code,
l.ItemDim3Code
FROM dbo.trOrderLine l
LEFT JOIN dbo.prItemVariant pv
ON pv.ItemTypeCode = l.ItemTypeCode
AND pv.ItemCode = l.ItemCode
AND pv.ColorCode = l.ColorCode
AND ISNULL(pv.ItemDim1Code,'') = ISNULL(l.ItemDim1Code,'')
AND ISNULL(pv.ItemDim2Code,'') = ISNULL(l.ItemDim2Code,'')
AND ISNULL(pv.ItemDim3Code,'') = ISNULL(l.ItemDim3Code,'')
WHERE l.OrderHeaderID = @p1
AND ISNULL(l.ItemCode,'') LIKE 'U%'
AND pv.ItemCode IS NULL
),
MaxPlu AS (
SELECT ISNULL(MAX(PLU),0) AS BasePlu
FROM dbo.prItemVariant WITH (UPDLOCK, HOLDLOCK)
)
INSERT INTO dbo.prItemVariant (
ItemTypeCode,
ItemCode,
ColorCode,
ItemDim1Code,
ItemDim2Code,
ItemDim3Code,
PLU,
CreatedUserName,
CreatedDate,
LastUpdatedUserName,
LastUpdatedDate
)
SELECT
m.ItemTypeCode,
m.ItemCode,
m.ColorCode,
m.ItemDim1Code,
m.ItemDim2Code,
m.ItemDim3Code,
mp.BasePlu + ROW_NUMBER() OVER (ORDER BY m.ItemCode, m.ColorCode, m.ItemDim1Code, m.ItemDim2Code, m.ItemDim3Code),
@p2,
GETDATE(),
@p2,
GETDATE()
FROM Missing m
CROSS JOIN MaxPlu mp;
`
res, err := mssql.Exec(query, orderHeaderID, username)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
// ========================================================
// OrderProductionUpdate - variant kontrolu ve guncelleme
// ========================================================
func GetOrderLineDims(mssql *sql.DB, orderHeaderID string, orderLineID string) (int16, string, string, string, error) {
var itemTypeCode int16
var dim1 string
var dim2 string
var dim3 string
err := mssql.QueryRow(`
SELECT
ItemTypeCode,
ISNULL(ItemDim1Code,'') AS ItemDim1Code,
ISNULL(ItemDim2Code,'') AS ItemDim2Code,
ISNULL(ItemDim3Code,'') AS ItemDim3Code
FROM dbo.trOrderLine
WHERE OrderHeaderID = @p1 AND OrderLineID = @p2
`, orderHeaderID, orderLineID).Scan(&itemTypeCode, &dim1, &dim2, &dim3)
return itemTypeCode, dim1, dim2, dim3, err
}
func VariantExists(mssql *sql.DB, itemTypeCode int16, itemCode string, colorCode string, dim1 string, dim2 string, dim3 string) (bool, error) {
var exists int
err := mssql.QueryRow(`
SELECT TOP 1 1
FROM dbo.prItemVariant
WHERE ItemTypeCode = @p1
AND ItemCode = @p2
AND ColorCode = @p3
AND ISNULL(ItemDim1Code,'') = ISNULL(@p4,'')
AND ISNULL(ItemDim2Code,'') = ISNULL(@p5,'')
AND ISNULL(ItemDim3Code,'') = ISNULL(@p6,'')
`, itemTypeCode, itemCode, colorCode, dim1, dim2, dim3).Scan(&exists)
if err == sql.ErrNoRows {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
func InsertMissingVariantsTx(tx *sql.Tx, missing []models.OrderProductionMissingVariant, username string) (int64, error) {
if len(missing) == 0 {
return 0, nil
}
var basePlu int64
if err := tx.QueryRow(`
SELECT ISNULL(MAX(PLU),0) AS BasePlu
FROM dbo.prItemVariant WITH (UPDLOCK, HOLDLOCK)
`).Scan(&basePlu); err != nil {
return 0, err
}
var inserted int64
for i, v := range missing {
plu := basePlu + int64(i) + 1
res, err := tx.Exec(`
IF NOT EXISTS (
SELECT 1
FROM dbo.prItemVariant
WHERE ItemTypeCode = @p1
AND ItemCode = @p2
AND ColorCode = @p3
AND ISNULL(ItemDim1Code,'') = ISNULL(@p4,'')
AND ISNULL(ItemDim2Code,'') = ISNULL(@p5,'')
AND ISNULL(ItemDim3Code,'') = ISNULL(@p6,'')
)
INSERT INTO dbo.prItemVariant (
ItemTypeCode,
ItemCode,
ColorCode,
ItemDim1Code,
ItemDim2Code,
ItemDim3Code,
PLU,
CreatedUserName,
CreatedDate,
LastUpdatedUserName,
LastUpdatedDate
)
VALUES (
@p1, @p2, @p3, @p4, @p5, @p6,
@p7, @p8, GETDATE(), @p8, GETDATE()
);
`, v.ItemTypeCode, v.ItemCode, v.ColorCode, v.ItemDim1Code, v.ItemDim2Code, v.ItemDim3Code, plu, username)
if err != nil {
return inserted, err
}
if rows, err := res.RowsAffected(); err == nil {
inserted += rows
}
}
return inserted, nil
}
func UpdateOrderLinesTx(tx *sql.Tx, orderHeaderID string, lines []models.OrderProductionUpdateLine, username string) (int64, error) {
var updated int64
for _, line := range lines {
res, err := tx.Exec(`
UPDATE dbo.trOrderLine
SET
ItemCode = @p1,
ColorCode = @p2,
ItemDim2Code = @p3,
LineDescription = COALESCE(NULLIF(@p4,''), LineDescription),
LastUpdatedUserName = @p5,
LastUpdatedDate = GETDATE()
WHERE OrderHeaderID = @p6 AND OrderLineID = @p7
`, line.NewItemCode, line.NewColor, line.NewDim2, line.NewDesc, username, orderHeaderID, line.OrderLineID)
if err != nil {
return updated, err
}
if rows, err := res.RowsAffected(); err == nil {
updated += rows
}
}
return updated, nil
}

View File

@@ -0,0 +1,269 @@
package queries
import (
"bssapp-backend/auth"
"bssapp-backend/internal/authz"
"context"
"database/sql"
"fmt"
)
// ========================================================
// 📌 GetOrderProductionList — Üretime verilecek ürün içeren siparişler
// ========================================================
func GetOrderProductionList(
ctx context.Context,
mssql *sql.DB,
pg *sql.DB,
search string,
) (*sql.Rows, error) {
claims, ok := auth.GetClaimsFromContext(ctx)
if !ok || claims == nil {
return nil, fmt.Errorf("unauthorized: claims not found")
}
// ----------------------------------------------------
// 🔐 PIYASA FILTER (ADMIN BYPASS)
// ----------------------------------------------------
piyasaWhere := "1=1"
if !claims.IsAdmin() {
codes, err := authz.GetUserPiyasaCodes(pg, int(claims.ID))
if err != nil {
return nil, fmt.Errorf("piyasa codes load error: %w", err)
}
if len(codes) == 0 {
piyasaWhere = "1=0"
} else {
piyasaWhere = authz.BuildINClause(
"UPPER(f2.CustomerAtt01)",
codes,
)
}
}
// ----------------------------------------------------
// 📄 BASE QUERY (orderlist.go ile aynı kolonlar)
// ----------------------------------------------------
baseQuery := fmt.Sprintf(`
SELECT
CAST(h.OrderHeaderID AS NVARCHAR(50)) AS OrderHeaderID,
ISNULL(h.OrderNumber, '') AS OrderNumber,
CONVERT(varchar, h.OrderDate, 23) AS OrderDate,
ISNULL(h.CurrAccCode, '') AS CurrAccCode,
ISNULL(ca.CurrAccDescription, '') AS CurrAccDescription,
ISNULL(mt.AttributeDescription, '') AS MusteriTemsilcisi,
ISNULL(py.AttributeDescription, '') AS Piyasa,
CONVERT(varchar, h.CreditableConfirmedDate,23) AS CreditableConfirmedDate,
ISNULL(h.DocCurrencyCode,'TRY') AS DocCurrencyCode,
ISNULL(l.TotalAmount,0) AS TotalAmount,
----------------------------------------------------------------
-- USD HESABI (TRY / EUR / GBP / USD DESTEKLİ)
----------------------------------------------------------------
CASE
WHEN h.DocCurrencyCode = 'USD'
THEN ISNULL(l.TotalAmount,0)
WHEN h.DocCurrencyCode = 'TRY'
AND usd.Rate > 0
THEN ISNULL(l.TotalAmount,0) / usd.Rate
WHEN h.DocCurrencyCode IN ('EUR','GBP')
AND cur.Rate > 0
AND usd.Rate > 0
THEN (ISNULL(l.TotalAmount,0) * cur.Rate) / usd.Rate
ELSE 0
END AS TotalAmountUSD,
ISNULL(l.PackedAmount,0) AS PackedAmount,
CASE
WHEN h.DocCurrencyCode = 'USD'
THEN ISNULL(l.PackedAmount,0)
WHEN h.DocCurrencyCode = 'TRY'
AND usd.Rate > 0
THEN ISNULL(l.PackedTRY,0) / usd.Rate
WHEN cur.Rate > 0
AND usd.Rate > 0
THEN (ISNULL(l.PackedAmount,0) * cur.Rate) / usd.Rate
ELSE 0
END AS PackedUSD,
CASE
WHEN ISNULL(l.TotalAmount,0) > 0
THEN (ISNULL(l.PackedAmount,0) * 100.0) / NULLIF(l.TotalAmount,0)
ELSE 0
END AS PackedRatePct,
ISNULL(h.IsCreditableConfirmed,0) AS IsCreditableConfirmed,
CAST(1 AS bit) AS HasUretimUrunu,
ISNULL(h.Description,'') AS Description,
usd.Rate AS ExchangeRateUSD
FROM dbo.trOrderHeader h
-- ✅ TOPLAM ARTIK trOrderLineCurrency'den: CurrencyCode = DocCurrencyCode
JOIN (
SELECT
l.OrderHeaderID,
SUM(ISNULL(c.NetAmount,0)) AS TotalAmount,
SUM(
CASE
WHEN ISNULL(c.CurrencyCode,'') = 'TRY'
THEN ISNULL(c.NetAmount,0)
ELSE 0
END
) AS TotalTRY,
SUM(
CASE
WHEN ISNULL(l.IsClosed,0) = 1
THEN ISNULL(c.NetAmount,0)
ELSE 0
END
) AS PackedAmount,
SUM(
CASE
WHEN ISNULL(l.IsClosed,0) = 1
AND ISNULL(c.CurrencyCode,'') = 'TRY'
THEN ISNULL(c.NetAmount,0)
ELSE 0
END
) AS PackedTRY
FROM dbo.trOrderLine l
JOIN dbo.trOrderHeader h2
ON h2.OrderHeaderID = l.OrderHeaderID
LEFT JOIN dbo.trOrderLineCurrency c
ON c.OrderLineID = l.OrderLineID
AND c.CurrencyCode = ISNULL(h2.DocCurrencyCode,'TRY')
GROUP BY l.OrderHeaderID
) l
ON l.OrderHeaderID = h.OrderHeaderID
LEFT JOIN dbo.cdCurrAccDesc ca
ON ca.CurrAccCode = h.CurrAccCode
AND ca.LangCode = 'TR'
-- müşteri temsilcisi + piyasa açıklamaları
LEFT JOIN dbo.CustomerAttributesFilter f
ON f.CurrAccCode = h.CurrAccCode
LEFT JOIN dbo.cdCurrAccAttributeDesc mt
ON mt.CurrAccTypeCode = 3
AND mt.AttributeTypeCode = 2
AND mt.AttributeCode = f.CustomerAtt02
AND mt.LangCode = 'TR'
LEFT JOIN dbo.cdCurrAccAttributeDesc py
ON py.CurrAccTypeCode = 3
AND py.AttributeTypeCode = 1
AND py.AttributeCode = f.CustomerAtt01
AND py.LangCode = 'TR'
----------------------------------------------------------------
-- USD → TRY
----------------------------------------------------------------
OUTER APPLY (
SELECT TOP 1 Rate
FROM dbo.AllExchangeRates
WHERE CurrencyCode = 'USD'
AND RelationCurrencyCode = 'TRY'
AND ExchangeTypeCode = 6
AND Rate > 0
AND Date <= CAST(GETDATE() AS date)
ORDER BY Date DESC
) usd
----------------------------------------------------------------
-- ORDER PB → TRY
----------------------------------------------------------------
OUTER APPLY (
SELECT TOP 1 Rate
FROM dbo.AllExchangeRates
WHERE CurrencyCode = h.DocCurrencyCode
AND RelationCurrencyCode = 'TRY'
AND ExchangeTypeCode = 6
AND Rate > 0
AND Date <= CAST(GETDATE() AS date)
ORDER BY Date DESC
) cur
WHERE
ISNULL(h.IsCancelOrder,0) = 0
AND h.OrderTypeCode = 1
AND h.ProcessCode = 'WS'
AND h.IsClosed = 0
-- 🔐 PIYASA AUTHZ (EXISTS — SAĞLAM YOL)
AND EXISTS (
SELECT 1
FROM dbo.CustomerAttributesFilter f2
WHERE f2.CurrAccCode = h.CurrAccCode
AND %s
)
-- ✅ Üretime verilecek ürün filtresi
AND EXISTS (
SELECT 1
FROM dbo.trOrderLine l2
WHERE l2.OrderHeaderID = h.OrderHeaderID
AND ISNULL(l2.ItemCode,'') LIKE 'U%%'
)
`, piyasaWhere)
// ----------------------------------------------------
// 🔍 SEARCH FILTER (CASE + TR SAFE)
// ----------------------------------------------------
if search != "" {
baseQuery += `
AND EXISTS (
SELECT 1
FROM dbo.trOrderHeader h2
LEFT JOIN dbo.cdCurrAccDesc ca2
ON ca2.CurrAccCode = h2.CurrAccCode
AND ca2.LangCode = 'TR'
WHERE h2.OrderHeaderID = h.OrderHeaderID
AND (
LOWER(REPLACE(REPLACE(h2.OrderNumber,'İ','I'),'ı','i'))
COLLATE Latin1_General_CI_AI LIKE LOWER(@p1)
OR LOWER(REPLACE(REPLACE(h2.CurrAccCode,'İ','I'),'ı','i'))
COLLATE Latin1_General_CI_AI LIKE LOWER(@p1)
OR LOWER(REPLACE(REPLACE(ca2.CurrAccDescription,'İ','I'),'ı','i'))
COLLATE Latin1_General_CI_AI LIKE LOWER(@p1)
OR LOWER(REPLACE(REPLACE(h2.Description,'İ','I'),'ı','i'))
COLLATE Latin1_General_CI_AI LIKE LOWER(@p1)
)
)
`
}
// ----------------------------------------------------
// 📌 ORDER
// ----------------------------------------------------
baseQuery += `
ORDER BY h.CreatedDate DESC
`
// ----------------------------------------------------
// ▶ EXECUTE
// ----------------------------------------------------
if search != "" {
searchLike := fmt.Sprintf("%%%s%%", search)
return mssql.Query(baseQuery, searchLike)
}
return mssql.Query(baseQuery)
}

View File

@@ -10,54 +10,102 @@ import (
// Ana tabloyu getiren fonksiyon (Vue header tablosu için) // Ana tabloyu getiren fonksiyon (Vue header tablosu için)
func GetStatements(params models.StatementParams) ([]models.StatementHeader, error) { func GetStatements(params models.StatementParams) ([]models.StatementHeader, error) {
// AccountCode normalize: "ZLA0127" → "ZLA 0127" // AccountCode normalize: "ZLA0127" → "ZLA 0127"
if len(params.AccountCode) == 7 && strings.ContainsAny(params.AccountCode, "0123456789") { if len(params.AccountCode) == 7 && strings.ContainsAny(params.AccountCode, "0123456789") {
params.AccountCode = params.AccountCode[:3] + " " + params.AccountCode[3:] params.AccountCode = params.AccountCode[:3] + " " + params.AccountCode[3:]
} }
if strings.TrimSpace(params.LangCode) == "" {
params.LangCode = "TR"
}
// Parislemler []string → '1','2','3' // Parislemler []string → '1','2','3'
parislemFilter := "''" parislemFilter := "''"
if len(params.Parislemler) > 0 { if len(params.Parislemler) > 0 {
quoted := make([]string, len(params.Parislemler)) quoted := make([]string, 0, len(params.Parislemler))
for i, v := range params.Parislemler { for _, v := range params.Parislemler {
quoted[i] = fmt.Sprintf("'%s'", v) v = strings.TrimSpace(v)
if v == "" {
continue
} }
quoted = append(quoted, fmt.Sprintf("'%s'", strings.ReplaceAll(v, "'", "''")))
}
if len(quoted) > 0 {
parislemFilter = strings.Join(quoted, ",") parislemFilter = strings.Join(quoted, ",")
} }
}
query := fmt.Sprintf(` query := fmt.Sprintf(`
;WITH CurrDesc AS (
;WITH Opening AS (
SELECT SELECT
b.CurrAccCode AS Cari_Kod, CurrAccCode,
b.DocCurrencyCode AS Para_Birimi, MAX(CurrAccDescription) AS CurrAccDescription
SUM(c.Debit - c.Credit) AS Devir_Bakiyesi FROM cdCurrAccDesc
WHERE LangCode = @LangCode
GROUP BY CurrAccCode
),
/* =========================================================
✅ Bu aralıkta hareket var mı?
Varsa : Devir = startdate öncesi
Yoksa : Devir = enddate dahil (enddate itibariyle bakiye)
========================================================= */
HasMovement AS (
SELECT
CASE WHEN EXISTS (
SELECT 1
FROM trCurrAccBook b FROM trCurrAccBook b
INNER JOIN CurrAccBookATAttributesFilter f
ON f.CurrAccBookID = b.CurrAccBookID
AND f.ATAtt01 IN (%s)
WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%'
AND b.DocumentDate BETWEEN @startdate AND @enddate
) THEN 1 ELSE 0 END AS HasMov
),
/* =========================================================
✅ Opening (Devir) — TEK CARİ KOD ALTINDA KONSOLİDE
Cari_Kod = @Carikod (sabit)
========================================================= */
Opening AS (
SELECT
@Carikod AS Cari_Kod,
b.DocCurrencyCode AS Para_Birimi,
SUM(ISNULL(c.Debit,0) - ISNULL(c.Credit,0)) AS Devir_Bakiyesi
FROM trCurrAccBook b
CROSS JOIN HasMovement hm
INNER JOIN CurrAccBookATAttributesFilter f2
ON f2.CurrAccBookID = b.CurrAccBookID
AND f2.ATAtt01 IN (%s)
LEFT JOIN trCurrAccBookCurrency c LEFT JOIN trCurrAccBookCurrency c
ON c.CurrAccBookID = b.CurrAccBookID ON c.CurrAccBookID = b.CurrAccBookID
AND c.CurrencyCode = b.DocCurrencyCode AND c.CurrencyCode = b.DocCurrencyCode
WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%' WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%'
AND b.DocumentDate < @startdate AND (
(hm.HasMov = 1 AND b.DocumentDate < @startdate) -- hareket varsa: klasik devir
AND EXISTS ( OR (hm.HasMov = 0 AND b.DocumentDate <= @enddate) -- hareket yoksa: enddate itibariyle bakiye
SELECT 1
FROM CurrAccBookATAttributesFilter f2
WHERE f2.CurrAccBookID = b.CurrAccBookID
AND f2.ATAtt01 IN (%s)
) )
GROUP BY b.DocCurrencyCode
GROUP BY
b.CurrAccCode,
b.DocCurrencyCode
), ),
/* =========================================================
✅ Hareketler (Movements) — TEK CARİ KOD ALTINDA KONSOLİDE
Cari_Kod = @Carikod (sabit)
Running sadece aralıktaki hareketlerden gelir.
========================================================= */
Movements AS ( Movements AS (
SELECT SELECT
b.CurrAccCode AS Cari_Kod, @Carikod AS Cari_Kod,
d.CurrAccDescription AS Cari_Isim,
COALESCE(
(SELECT TOP 1 cd.CurrAccDescription
FROM CurrDesc cd
WHERE cd.CurrAccCode = @Carikod),
(SELECT TOP 1 cd.CurrAccDescription
FROM CurrDesc cd
WHERE cd.CurrAccCode LIKE '%%' + @Carikod + '%%'
ORDER BY cd.CurrAccCode)
) AS Cari_Isim,
CONVERT(varchar(10), b.DocumentDate, 23) AS Belge_Tarihi, CONVERT(varchar(10), b.DocumentDate, 23) AS Belge_Tarihi,
CONVERT(varchar(10), b.DueDate, 23) AS Vade_Tarihi, CONVERT(varchar(10), b.DueDate, 23) AS Vade_Tarihi,
@@ -68,38 +116,27 @@ Movements AS (
b.DocCurrencyCode AS Para_Birimi, b.DocCurrencyCode AS Para_Birimi,
c.Debit AS Borc, ISNULL(c.Debit,0) AS Borc,
c.Credit AS Alacak, ISNULL(c.Credit,0) AS Alacak,
SUM(c.Debit - c.Credit) SUM(ISNULL(c.Debit,0) - ISNULL(c.Credit,0))
OVER ( OVER (
PARTITION BY b.CurrAccCode, c.CurrencyCode PARTITION BY b.DocCurrencyCode
ORDER BY b.DocumentDate, b.CurrAccBookID ORDER BY b.DocumentDate, b.CurrAccBookID
) AS Hareket_Bakiyesi, ) AS Hareket_Bakiyesi,
f.ATAtt01 AS Parislemtipi f.ATAtt01 AS Parislemtipi
FROM trCurrAccBook b FROM trCurrAccBook b
INNER JOIN CurrAccBookATAttributesFilter f
LEFT JOIN cdCurrAccDesc d ON f.CurrAccBookID = b.CurrAccBookID
ON b.CurrAccCode = d.CurrAccCode AND f.ATAtt01 IN (%s)
LEFT JOIN trCurrAccBookCurrency c LEFT JOIN trCurrAccBookCurrency c
ON b.CurrAccBookID = c.CurrAccBookID ON c.CurrAccBookID = b.CurrAccBookID
AND b.DocCurrencyCode = c.CurrencyCode AND c.CurrencyCode = b.DocCurrencyCode
LEFT JOIN CurrAccBookATAttributesFilter f
ON b.CurrAccBookID = f.CurrAccBookID
WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%' WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%'
AND b.DocumentDate BETWEEN @startdate AND @enddate AND b.DocumentDate BETWEEN @startdate AND @enddate
AND EXISTS (
SELECT 1
FROM CurrAccBookATAttributesFilter f2
WHERE f2.CurrAccBookID = b.CurrAccBookID
AND f2.ATAtt01 IN (%s)
)
) )
SELECT SELECT
@@ -111,65 +148,63 @@ SELECT
m.Islem_Tipi, m.Islem_Tipi,
m.Aciklama, m.Aciklama,
m.Para_Birimi, m.Para_Birimi,
m.Borc, m.Borc,
m.Alacak, m.Alacak,
/* ✅ Bakiye = Devir + Aralıktaki Running */
ISNULL(o.Devir_Bakiyesi,0) + m.Hareket_Bakiyesi AS Bakiye, ISNULL(o.Devir_Bakiyesi,0) + m.Hareket_Bakiyesi AS Bakiye,
m.Parislemtipi AS Parislemler m.Parislemtipi AS Parislemler
FROM Movements m FROM Movements m
LEFT JOIN Opening o LEFT JOIN Opening o
ON o.Cari_Kod = m.Cari_Kod ON o.Cari_Kod = m.Cari_Kod
AND o.Para_Birimi = m.Para_Birimi AND o.Para_Birimi = m.Para_Birimi
UNION ALL UNION ALL
/* =========================================================
/* ✅ Devir satırı sadece Openingden */ ✅ Devir Satırı (kur bazında) — Opening'den gelir
Hareket varsa: startdate öncesi
Hareket yoksa: enddate itibariyle bakiye
========================================================= */
SELECT SELECT
o.Cari_Kod, o.Cari_Kod,
d.CurrAccDescription, COALESCE(
(SELECT TOP 1 cd.CurrAccDescription
FROM CurrDesc cd
WHERE cd.CurrAccCode = @Carikod),
(SELECT TOP 1 cd.CurrAccDescription
FROM CurrDesc cd
WHERE cd.CurrAccCode LIKE '%%' + @Carikod + '%%'
ORDER BY cd.CurrAccCode)
) AS Cari_Isim,
CONVERT(varchar(10), @startdate, 23), CONVERT(varchar(10), @startdate, 23) AS Belge_Tarihi,
CONVERT(varchar(10), @startdate, 23), CONVERT(varchar(10), @startdate, 23) AS Vade_Tarihi,
'Baslangic_devir', 'Baslangic_devir' AS Belge_No,
'Devir', 'Devir' AS Islem_Tipi,
'Devir Bakiyesi', 'Devir Bakiyesi' AS Aciklama,
o.Para_Birimi, o.Para_Birimi,
CASE CASE WHEN o.Devir_Bakiyesi >= 0 THEN o.Devir_Bakiyesi ELSE 0 END AS Borc,
WHEN o.Devir_Bakiyesi >= 0 THEN o.Devir_Bakiyesi CASE WHEN o.Devir_Bakiyesi < 0 THEN ABS(o.Devir_Bakiyesi) ELSE 0 END AS Alacak,
ELSE 0
END,
CASE o.Devir_Bakiyesi AS Bakiye,
WHEN o.Devir_Bakiyesi < 0 THEN ABS(o.Devir_Bakiyesi)
ELSE 0
END,
o.Devir_Bakiyesi, CAST(NULL AS varchar(32)) AS Parislemler
'%s'
FROM Opening o FROM Opening o
LEFT JOIN cdCurrAccDesc d
ON d.CurrAccCode = o.Cari_Kod
ORDER BY ORDER BY
Para_Birimi, Para_Birimi,
Belge_Tarihi; Belge_Tarihi;
`, `,
parislemFilter, parislemFilter, // HasMovement
parislemFilter, parislemFilter, // Opening
parislemFilter, parislemFilter, // Movements
) )
rows, err := db.MssqlDB.Query(query, rows, err := db.MssqlDB.Query(query,

View File

@@ -1,187 +1,18 @@
// queries/statements_header_pdf.go
package queries package queries
import ( import (
"bssapp-backend/db"
"bssapp-backend/models" "bssapp-backend/models"
"database/sql"
"fmt"
"log" "log"
"strings"
) )
// küçük yardımcı: boşlukları temizle, her değeri ayrı tırnakla sar
func buildQuotedHList(vals []string) string {
var pp []string
for _, v := range vals {
v = strings.TrimSpace(v)
if v != "" {
pp = append(pp, fmt.Sprintf("'%s'", v)) // '1','2' gibi
}
}
if len(pp) == 0 {
return ""
}
return strings.Join(pp, ",")
}
/* ============================ HEADER (Ana Tablo) ============================ */
func GetStatementsHPDF(accountCode, startDate, endDate string, parislemler []string) ([]models.StatementHeader, []string, error) { func GetStatementsHPDF(accountCode, startDate, endDate string, parislemler []string) ([]models.StatementHeader, []string, error) {
// Account normalize headers, err := getStatementsForPDF(accountCode, startDate, endDate, parislemler)
if len(accountCode) == 7 && strings.ContainsAny(accountCode, "0123456789") {
accountCode = accountCode[:3] + " " + accountCode[3:]
}
// IN list parse et
inList := buildQuotedHList(parislemler)
parislemCond := "''"
if inList != "" {
parislemCond = inList
}
query := fmt.Sprintf(`
;WITH Opening AS (
SELECT
b.CurrAccCode AS Cari_Kod,
b.DocCurrencyCode AS Para_Birimi,
SUM(c.Debit - c.Credit) AS Devir_Bakiyesi
FROM trCurrAccBook b
LEFT JOIN trCurrAccBookCurrency c
ON c.CurrAccBookID = b.CurrAccBookID
AND c.CurrencyCode = b.DocCurrencyCode
WHERE b.CurrAccCode LIKE @Carikod
AND b.DocumentDate < @StartDate
AND EXISTS (
SELECT 1
FROM CurrAccBookATAttributesFilter f2
WHERE f2.CurrAccBookID = b.CurrAccBookID
AND f2.ATAtt01 IN (%s)
)
GROUP BY b.CurrAccCode, b.DocCurrencyCode
),
Movements AS (
SELECT
b.CurrAccCode AS Cari_Kod,
d.CurrAccDescription AS Cari_Isim,
CONVERT(varchar(10), b.DocumentDate, 23) AS Belge_Tarihi,
CONVERT(varchar(10), b.DueDate, 23) AS Vade_Tarihi,
b.RefNumber AS Belge_No,
b.BaseApplicationCode AS Islem_Tipi,
b.LineDescription AS Aciklama,
b.DocCurrencyCode AS Para_Birimi,
c.Debit AS Borc,
c.Credit AS Alacak,
SUM(c.Debit - c.Credit)
OVER (PARTITION BY b.CurrAccCode, c.CurrencyCode
ORDER BY b.DocumentDate, b.CurrAccBookID) AS Hareket_Bakiyesi,
f.ATAtt01 AS Parislemler
FROM trCurrAccBook b
LEFT JOIN cdCurrAccDesc d
ON b.CurrAccCode = d.CurrAccCode AND d.LangCode = 'TR'
LEFT JOIN trCurrAccBookCurrency c
ON b.CurrAccBookID = c.CurrAccBookID
AND b.DocCurrencyCode = c.CurrencyCode
LEFT JOIN CurrAccBookATAttributesFilter f
ON b.CurrAccBookID = f.CurrAccBookID
WHERE b.CurrAccCode LIKE @Carikod
AND b.DocumentDate BETWEEN @StartDate AND @EndDate
AND EXISTS (
SELECT 1
FROM CurrAccBookATAttributesFilter f2
WHERE f2.CurrAccBookID = b.CurrAccBookID
AND f2.ATAtt01 IN (%s)
)
)`, parislemCond, parislemCond)
query += fmt.Sprintf(`
SELECT
m.Cari_Kod,
m.Cari_Isim,
m.Belge_Tarihi,
m.Vade_Tarihi,
m.Belge_No,
m.Islem_Tipi,
m.Aciklama,
m.Para_Birimi,
m.Borc,
m.Alacak,
ISNULL(o.Devir_Bakiyesi,0) + m.Hareket_Bakiyesi AS Bakiye,
m.Parislemler
FROM Movements m
LEFT JOIN Opening o
ON o.Cari_Kod = m.Cari_Kod
AND o.Para_Birimi = m.Para_Birimi
UNION ALL
-- Devir satırı
SELECT
@Carikod AS Cari_Kod,
MAX(d.CurrAccDescription) AS Cari_Isim,
CONVERT(varchar(10), @StartDate, 23) AS Belge_Tarihi,
CONVERT(varchar(10), @StartDate, 23) AS Vade_Tarihi,
'Baslangic_devir' AS Belge_No,
'Devir' AS Islem_Tipi,
'Devir Bakiyesi' AS Aciklama,
b.DocCurrencyCode AS Para_Birimi,
SUM(c.Debit) AS Borc,
SUM(c.Credit) AS Alacak,
SUM(c.Debit) - SUM(c.Credit) AS Bakiye,
(
SELECT STRING_AGG(x.ATAtt01, ',')
FROM (
SELECT DISTINCT f2.ATAtt01
FROM CurrAccBookATAttributesFilter f2
INNER JOIN trCurrAccBook bb
ON f2.CurrAccBookID = bb.CurrAccBookID
WHERE bb.CurrAccCode LIKE @Carikod
AND bb.DocumentDate < @StartDate
AND f2.ATAtt01 IN (%s)
) x
) AS Parislemler
FROM trCurrAccBook b
LEFT JOIN cdCurrAccDesc d
ON b.CurrAccCode = d.CurrAccCode AND d.LangCode = 'TR'
LEFT JOIN trCurrAccBookCurrency c
ON b.CurrAccBookID = c.CurrAccBookID
AND b.DocCurrencyCode = c.CurrencyCode
WHERE b.CurrAccCode LIKE @Carikod
AND b.DocumentDate < @StartDate
GROUP BY b.DocCurrencyCode
ORDER BY Para_Birimi, Belge_Tarihi;`, parislemCond)
rows, err := db.MssqlDB.Query(query,
sql.Named("Carikod", "%"+accountCode+"%"),
sql.Named("StartDate", startDate),
sql.Named("EndDate", endDate),
)
if err != nil { if err != nil {
log.Printf("Header sorgu hatası: %v", err) log.Printf("Header query error: %v", err)
return nil, nil, fmt.Errorf("header sorgu hatası: %v", err)
}
defer rows.Close()
var headers []models.StatementHeader
var belgeNos []string
for rows.Next() {
var h models.StatementHeader
if err := rows.Scan(
&h.CariKod, &h.CariIsim,
&h.BelgeTarihi, &h.VadeTarihi,
&h.BelgeNo, &h.IslemTipi,
&h.Aciklama, &h.ParaBirimi,
&h.Borc, &h.Alacak,
&h.Bakiye, &h.Parislemler,
); err != nil {
log.Printf("❌ Header scan hatası: %v", err)
return nil, nil, err return nil, nil, err
} }
headers = append(headers, h)
if h.BelgeNo != "" { belgeNos := collectBelgeNos(headers)
belgeNos = append(belgeNos, h.BelgeNo) log.Printf("Header rows fetched: %d, belge no count: %d", len(headers), len(belgeNos))
}
}
log.Printf("✅ Header verileri alındı: %d kayıt, %d belge no", len(headers), len(belgeNos))
return headers, belgeNos, nil return headers, belgeNos, nil
} }

View File

@@ -0,0 +1,35 @@
package queries
import "bssapp-backend/models"
func getStatementsForPDF(
accountCode string,
startDate string,
endDate string,
parislemler []string,
) ([]models.StatementHeader, error) {
return GetStatements(models.StatementParams{
AccountCode: accountCode,
StartDate: startDate,
EndDate: endDate,
LangCode: "TR",
Parislemler: parislemler,
})
}
func collectBelgeNos(headers []models.StatementHeader) []string {
seen := make(map[string]struct{}, len(headers))
out := make([]string, 0, len(headers))
for _, h := range headers {
no := h.BelgeNo
if no == "" || no == "Baslangic_devir" {
continue
}
if _, ok := seen[no]; ok {
continue
}
seen[no] = struct{}{}
out = append(out, no)
}
return out
}

View File

@@ -10,179 +10,15 @@ import (
"strings" "strings"
) )
// küçük yardımcı: boşlukları temizle, her değeri ayrı tırnakla sar
func buildQuotedList(vals []string) string {
var pp []string
for _, v := range vals {
v = strings.TrimSpace(v)
if v != "" {
pp = append(pp, fmt.Sprintf("'%s'", v)) // '1','2' gibi
}
}
if len(pp) == 0 {
return ""
}
return strings.Join(pp, ",")
}
/* ============================ HEADER (Ana Tablo) ============================ */
func GetStatementsPDF(accountCode, startDate, endDate string, parislemler []string) ([]models.StatementHeader, []string, error) { func GetStatementsPDF(accountCode, startDate, endDate string, parislemler []string) ([]models.StatementHeader, []string, error) {
// Account normalize headers, err := getStatementsForPDF(accountCode, startDate, endDate, parislemler)
if len(accountCode) == 7 && strings.ContainsAny(accountCode, "0123456789") {
accountCode = accountCode[:3] + " " + accountCode[3:]
}
// IN list parse et
inList := buildQuotedList(parislemler)
parislemCond := "''"
if inList != "" {
parislemCond = inList
}
query := fmt.Sprintf(`
;WITH Opening AS (
SELECT
b.CurrAccCode AS Cari_Kod,
b.DocCurrencyCode AS Para_Birimi,
SUM(c.Debit - c.Credit) AS Devir_Bakiyesi
FROM trCurrAccBook b
LEFT JOIN trCurrAccBookCurrency c
ON c.CurrAccBookID = b.CurrAccBookID
AND c.CurrencyCode = b.DocCurrencyCode
WHERE b.CurrAccCode LIKE @Carikod
AND b.DocumentDate < @StartDate
AND EXISTS (
SELECT 1
FROM CurrAccBookATAttributesFilter f2
WHERE f2.CurrAccBookID = b.CurrAccBookID
AND f2.ATAtt01 IN (%s)
)
GROUP BY b.CurrAccCode, b.DocCurrencyCode
),
Movements AS (
SELECT
b.CurrAccCode AS Cari_Kod,
d.CurrAccDescription AS Cari_Isim,
CONVERT(varchar(10), b.DocumentDate, 23) AS Belge_Tarihi,
CONVERT(varchar(10), b.DueDate, 23) AS Vade_Tarihi,
b.RefNumber AS Belge_No,
b.BaseApplicationCode AS Islem_Tipi,
b.LineDescription AS Aciklama,
b.DocCurrencyCode AS Para_Birimi,
c.Debit AS Borc,
c.Credit AS Alacak,
SUM(c.Debit - c.Credit)
OVER (PARTITION BY b.CurrAccCode, c.CurrencyCode
ORDER BY b.DocumentDate, b.CurrAccBookID) AS Hareket_Bakiyesi,
f.ATAtt01 AS Parislemler
FROM trCurrAccBook b
LEFT JOIN cdCurrAccDesc d
ON b.CurrAccCode = d.CurrAccCode AND d.LangCode = 'TR'
LEFT JOIN trCurrAccBookCurrency c
ON b.CurrAccBookID = c.CurrAccBookID
AND b.DocCurrencyCode = c.CurrencyCode
LEFT JOIN CurrAccBookATAttributesFilter f
ON b.CurrAccBookID = f.CurrAccBookID
WHERE b.CurrAccCode LIKE @Carikod
AND b.DocumentDate BETWEEN @StartDate AND @EndDate
AND EXISTS (
SELECT 1
FROM CurrAccBookATAttributesFilter f2
WHERE f2.CurrAccBookID = b.CurrAccBookID
AND f2.ATAtt01 IN (%s)
)
)`, parislemCond, parislemCond)
query += fmt.Sprintf(`
SELECT
m.Cari_Kod,
m.Cari_Isim,
m.Belge_Tarihi,
m.Vade_Tarihi,
m.Belge_No,
m.Islem_Tipi,
m.Aciklama,
m.Para_Birimi,
m.Borc,
m.Alacak,
ISNULL(o.Devir_Bakiyesi,0) + m.Hareket_Bakiyesi AS Bakiye,
m.Parislemler
FROM Movements m
LEFT JOIN Opening o
ON o.Cari_Kod = m.Cari_Kod
AND o.Para_Birimi = m.Para_Birimi
UNION ALL
-- Devir satırı
SELECT
@Carikod AS Cari_Kod,
MAX(d.CurrAccDescription) AS Cari_Isim,
CONVERT(varchar(10), @StartDate, 23) AS Belge_Tarihi,
CONVERT(varchar(10), @StartDate, 23) AS Vade_Tarihi,
'Baslangic_devir' AS Belge_No,
'Devir' AS Islem_Tipi,
'Devir Bakiyesi' AS Aciklama,
b.DocCurrencyCode AS Para_Birimi,
SUM(c.Debit) AS Borc,
SUM(c.Credit) AS Alacak,
SUM(c.Debit) - SUM(c.Credit) AS Bakiye,
(
SELECT STRING_AGG(x.ATAtt01, ',')
FROM (
SELECT DISTINCT f2.ATAtt01
FROM CurrAccBookATAttributesFilter f2
INNER JOIN trCurrAccBook bb
ON f2.CurrAccBookID = bb.CurrAccBookID
WHERE bb.CurrAccCode LIKE @Carikod
AND bb.DocumentDate < @StartDate
AND f2.ATAtt01 IN (%s)
) x
) AS Parislemler
FROM trCurrAccBook b
LEFT JOIN cdCurrAccDesc d
ON b.CurrAccCode = d.CurrAccCode AND d.LangCode = 'TR'
LEFT JOIN trCurrAccBookCurrency c
ON b.CurrAccBookID = c.CurrAccBookID
AND b.DocCurrencyCode = c.CurrencyCode
WHERE b.CurrAccCode LIKE @Carikod
AND b.DocumentDate < @StartDate
GROUP BY b.DocCurrencyCode
ORDER BY Para_Birimi, Belge_Tarihi;`, parislemCond)
rows, err := db.MssqlDB.Query(query,
sql.Named("Carikod", "%"+accountCode+"%"),
sql.Named("StartDate", startDate),
sql.Named("EndDate", endDate),
)
if err != nil { if err != nil {
log.Printf("Header sorgu hatası: %v", err) log.Printf("Header query error: %v", err)
return nil, nil, fmt.Errorf("header sorgu hatası: %v", err)
}
defer rows.Close()
var headers []models.StatementHeader
var belgeNos []string
for rows.Next() {
var h models.StatementHeader
if err := rows.Scan(
&h.CariKod, &h.CariIsim,
&h.BelgeTarihi, &h.VadeTarihi,
&h.BelgeNo, &h.IslemTipi,
&h.Aciklama, &h.ParaBirimi,
&h.Borc, &h.Alacak,
&h.Bakiye, &h.Parislemler,
); err != nil {
log.Printf("❌ Header scan hatası: %v", err)
return nil, nil, err return nil, nil, err
} }
headers = append(headers, h)
if h.BelgeNo != "" { belgeNos := collectBelgeNos(headers)
belgeNos = append(belgeNos, h.BelgeNo) log.Printf("Header rows fetched: %d, belge no count: %d", len(headers), len(belgeNos))
}
}
log.Printf("✅ Header verileri alındı: %d kayıt, %d belge no", len(headers), len(belgeNos))
return headers, belgeNos, nil return headers, belgeNos, nil
} }
@@ -191,7 +27,7 @@ ORDER BY Para_Birimi, Belge_Tarihi;`, parislemCond)
func GetDetailsMapPDF(belgeNos []string, startDate, endDate string) (map[string][]models.StatementDetail, error) { func GetDetailsMapPDF(belgeNos []string, startDate, endDate string) (map[string][]models.StatementDetail, error) {
result := make(map[string][]models.StatementDetail) result := make(map[string][]models.StatementDetail)
if len(belgeNos) == 0 { if len(belgeNos) == 0 {
log.Println("⚠️ GetDetailsMapPDF: belge listesi boş") log.Println("GetDetailsMapPDF: belge listesi bos")
return result, nil return result, nil
} }
@@ -219,7 +55,11 @@ SELECT
MAX(ISNULL(KisaKarDesc.AttributeDescription, '')) AS Icerik, MAX(ISNULL(KisaKarDesc.AttributeDescription, '')) AS Icerik,
a.ItemCode, a.ColorCode, a.ItemCode, a.ColorCode,
SUM(a.Qty1), SUM(ABS(a.Doc_Price)), SUM(a.Qty1),
CAST(
SUM(a.Qty1 * ABS(a.Doc_Price))
/ NULLIF(SUM(a.Qty1), 0)
AS numeric(18,4)),
CAST(SUM(a.Qty1 * ABS(a.Doc_Price)) AS numeric(18,2)) CAST(SUM(a.Qty1 * ABS(a.Doc_Price)) AS numeric(18,2))
FROM AllInvoicesWithAttributes a FROM AllInvoicesWithAttributes a
@@ -258,7 +98,7 @@ LEFT JOIN cdItemAttributeDesc FitDesc
AND FitTbl.AttributeCode = FitDesc.AttributeCode AND FitTbl.AttributeCode = FitDesc.AttributeCode
AND FitTbl.ItemTypeCode = FitDesc.ItemTypeCode AND FitTbl.ItemTypeCode = FitDesc.ItemTypeCode
-- Kısa Karışım -- Kisa Karisim
LEFT JOIN prItemAttribute KisaKar LEFT JOIN prItemAttribute KisaKar
ON a.ItemCode = KisaKar.ItemCode AND KisaKar.AttributeTypeCode = 41 ON a.ItemCode = KisaKar.ItemCode AND KisaKar.AttributeTypeCode = 41
LEFT JOIN cdItemAttributeDesc KisaKarDesc LEFT JOIN cdItemAttributeDesc KisaKarDesc
@@ -274,8 +114,8 @@ ORDER BY a.InvoiceNumber, a.ItemCode, a.ColorCode;`, inBelge)
sql.Named("EndDate", endDate), sql.Named("EndDate", endDate),
) )
if err != nil { if err != nil {
log.Printf("Detay sorgu hatası: %v", err) log.Printf("Detail query error: %v", err)
return nil, fmt.Errorf("detay sorgu hatası: %v", err) return nil, fmt.Errorf("detay sorgu hatasi: %v", err)
} }
defer rows.Close() defer rows.Close()
@@ -295,11 +135,11 @@ ORDER BY a.InvoiceNumber, a.ItemCode, a.ColorCode;`, inBelge)
&d.ToplamFiyat, &d.ToplamFiyat,
&d.ToplamTutar, &d.ToplamTutar,
); err != nil { ); err != nil {
log.Printf("Detay scan hatası: %v", err) log.Printf("Detail scan error: %v", err)
return nil, err return nil, err
} }
result[d.BelgeRefNumarasi] = append(result[d.BelgeRefNumarasi], d) result[d.BelgeRefNumarasi] = append(result[d.BelgeRefNumarasi], d)
} }
log.Printf("Detay verileri alındı: %d belge için detay var", len(result)) log.Printf("Detail rows fetched for %d belge", len(result))
return result, nil return result, nil
} }

View File

@@ -234,11 +234,18 @@ func (r *UserRepository) GetLegacyUserForLogin(login string) (*models.User, erro
COALESCE(u.upass,'') as upass, COALESCE(u.upass,'') as upass,
u.is_active, u.is_active,
COALESCE(u.email,''), COALESCE(u.email,''),
COALESCE(u.dfrole_id,0) as role_id, COALESCE(ru.dfrole_id,0) as role_id,
COALESCE(dr.code,'') as role_code, COALESCE(dr.code,'') as role_code,
COALESCE(u.force_password_change,false) COALESCE(u.force_password_change,false)
FROM dfusr u FROM dfusr u
LEFT JOIN dfrole dr ON dr.id = u.dfrole_id LEFT JOIN LATERAL (
SELECT dfrole_id
FROM dfrole_usr
WHERE dfusr_id = u.id
ORDER BY dfrole_id
LIMIT 1
) ru ON true
LEFT JOIN dfrole dr ON dr.id = ru.dfrole_id
WHERE u.is_active = true WHERE u.is_active = true
AND ( AND (
LOWER(u.code) = LOWER($1) LOWER(u.code) = LOWER($1)

View File

@@ -62,7 +62,7 @@ func AdminResetPasswordHandler(db *sql.DB) http.HandlerFunc {
// --------------------------------------------------- // ---------------------------------------------------
// 4⃣ UPDATE mk_dfusr // 4⃣ UPDATE mk_dfusr
// --------------------------------------------------- // ---------------------------------------------------
_, err = db.Exec(` res, err := db.Exec(`
UPDATE mk_dfusr UPDATE mk_dfusr
SET SET
password_hash = $1, password_hash = $1,
@@ -77,6 +77,24 @@ func AdminResetPasswordHandler(db *sql.DB) http.HandlerFunc {
return return
} }
affected, _ := res.RowsAffected()
if affected == 0 {
_, err = db.Exec(`
UPDATE dfusr
SET
upass = $1,
force_password_change = true,
last_updated_date = NOW()
WHERE id = $2
AND is_active = true
`, string(hash), userID)
if err != nil {
http.Error(w, "legacy password reset failed", http.StatusInternalServerError)
return
}
}
// --------------------------------------------------- // ---------------------------------------------------
// 5⃣ REFRESH TOKEN REVOKE // 5⃣ REFRESH TOKEN REVOKE
// --------------------------------------------------- // ---------------------------------------------------

View File

@@ -29,7 +29,7 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
r.Method, r.Method,
r.URL.Path, r.URL.Path,
) )
http.Error(w, "unauthorized: token missing or invalid", http.StatusUnauthorized) http.Error(w, "yetkisiz: token eksik veya geçersiz", http.StatusUnauthorized)
return return
} }
@@ -39,14 +39,14 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
} }
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid payload", http.StatusBadRequest) http.Error(w, "geçersiz istek gövdesi", http.StatusBadRequest)
return return
} }
req.CurrentPassword = strings.TrimSpace(req.CurrentPassword) req.CurrentPassword = strings.TrimSpace(req.CurrentPassword)
req.NewPassword = strings.TrimSpace(req.NewPassword) req.NewPassword = strings.TrimSpace(req.NewPassword)
if req.CurrentPassword == "" || req.NewPassword == "" { if req.CurrentPassword == "" || req.NewPassword == "" {
http.Error(w, "password fields required", http.StatusUnprocessableEntity) http.Error(w, "şifre alanları zorunludur", http.StatusUnprocessableEntity)
return return
} }
@@ -61,7 +61,7 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
claims.ID, claims.ID,
mkErr, mkErr,
) )
http.Error(w, "user lookup failed", http.StatusInternalServerError) http.Error(w, "kullanıcı sorgulama hatası", http.StatusInternalServerError)
return return
} }
@@ -79,20 +79,30 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
claims.ID, claims.ID,
claims.Username, claims.Username,
) )
http.Error(w, "mevcut sifre hatali", http.StatusUnauthorized) http.Error(w, "mevcut şifre hatalı", http.StatusUnauthorized)
return return
} }
} else { } else {
var err error var err error
legacyUser, err = legacyRepo.GetLegacyUserForLogin(claims.Username) legacyUser, err = legacyRepo.GetLegacyUserForLogin(claims.Username)
if err != nil || legacyUser == nil || !legacyUser.IsActive || int64(legacyUser.ID) != claims.ID { if err != nil || legacyUser == nil || !legacyUser.IsActive {
log.Printf( log.Printf(
"FIRST_PASSWORD_CHANGE 401 reason=legacy_user_not_found user_id=%d username=%s err=%v", "FIRST_PASSWORD_CHANGE 401 reason=legacy_user_not_found user_id=%d username=%s err=%v",
claims.ID, claims.ID,
claims.Username, claims.Username,
err, err,
) )
http.Error(w, "unauthorized: user not found", http.StatusUnauthorized) http.Error(w, "yetkisiz: kullanıcı bulunamadı", http.StatusUnauthorized)
return
}
if !hasMkUser && int64(legacyUser.ID) != claims.ID {
log.Printf(
"FIRST_PASSWORD_CHANGE 401 reason=legacy_id_mismatch user_id=%d legacy_id=%d username=%s",
claims.ID,
legacyUser.ID,
claims.Username,
)
http.Error(w, "yetkisiz: kullanıcı bulunamadı", http.StatusUnauthorized)
return return
} }
@@ -102,7 +112,7 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
claims.ID, claims.ID,
claims.Username, claims.Username,
) )
http.Error(w, "mevcut sifre hatali", http.StatusUnauthorized) http.Error(w, "mevcut şifre hatalı", http.StatusUnauthorized)
return return
} }
} }
@@ -117,13 +127,13 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
bcrypt.DefaultCost, bcrypt.DefaultCost,
) )
if err != nil { if err != nil {
http.Error(w, "password hash error", http.StatusInternalServerError) http.Error(w, "şifre hash hatası", http.StatusInternalServerError)
return return
} }
tx, err := db.Begin() tx, err := db.Begin()
if err != nil { if err != nil {
http.Error(w, "transaction error", http.StatusInternalServerError) http.Error(w, "işlem başlatılamadı", http.StatusInternalServerError)
return return
} }
defer tx.Rollback() defer tx.Rollback()
@@ -146,7 +156,7 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
claims.ID, claims.ID,
err, err,
) )
http.Error(w, "password update failed", http.StatusInternalServerError) http.Error(w, "şifre güncellenemedi", http.StatusInternalServerError)
return return
} }
@@ -156,21 +166,31 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
"FIRST_PASSWORD_CHANGE 500 reason=password_update_no_rows user_id=%d", "FIRST_PASSWORD_CHANGE 500 reason=password_update_no_rows user_id=%d",
claims.ID, claims.ID,
) )
http.Error(w, "password update failed", http.StatusInternalServerError) http.Error(w, "şifre güncellenemedi", http.StatusInternalServerError)
return return
} }
} else { } else {
if legacyUser == nil { if legacyUser == nil {
// Defensive fallback, should not happen. // Defensive fallback, should not happen.
legacyUser, err = legacyRepo.GetLegacyUserForLogin(claims.Username) legacyUser, err = legacyRepo.GetLegacyUserForLogin(claims.Username)
if err != nil || legacyUser == nil || int64(legacyUser.ID) != claims.ID { if err != nil || legacyUser == nil {
log.Printf( log.Printf(
"FIRST_PASSWORD_CHANGE 500 reason=legacy_reload_failed user_id=%d username=%s err=%v", "FIRST_PASSWORD_CHANGE 500 reason=legacy_reload_failed user_id=%d username=%s err=%v",
claims.ID, claims.ID,
claims.Username, claims.Username,
err, err,
) )
http.Error(w, "legacy user reload failed", http.StatusInternalServerError) http.Error(w, "legacy kullanıcı yeniden yüklenemedi", http.StatusInternalServerError)
return
}
if !hasMkUser && int64(legacyUser.ID) != claims.ID {
log.Printf(
"FIRST_PASSWORD_CHANGE 500 reason=legacy_reload_id_mismatch user_id=%d legacy_id=%d username=%s",
claims.ID,
legacyUser.ID,
claims.Username,
)
http.Error(w, "legacy kullanıcı yeniden yüklenemedi", http.StatusInternalServerError)
return return
} }
} }
@@ -222,7 +242,7 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
claims.Username, claims.Username,
err, err,
) )
http.Error(w, "legacy migration failed", http.StatusInternalServerError) http.Error(w, "legacy geçişi başarısız", http.StatusInternalServerError)
return return
} }
@@ -235,7 +255,7 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
claims.ID, claims.ID,
err, err,
) )
http.Error(w, "commit failed", http.StatusInternalServerError) http.Error(w, "işlem tamamlanamadı", http.StatusInternalServerError)
return return
} }
@@ -262,7 +282,7 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
false, false,
) )
if err != nil { if err != nil {
http.Error(w, "token generation failed", http.StatusInternalServerError) http.Error(w, "token üretilemedi", http.StatusInternalServerError)
return return
} }

View File

@@ -30,6 +30,76 @@ type LoginRequest struct {
Password string `json:"password"` Password string `json:"password"`
} }
func looksLikeBcryptHash(value string) bool {
return strings.HasPrefix(value, "$2a$") ||
strings.HasPrefix(value, "$2b$") ||
strings.HasPrefix(value, "$2y$")
}
func ensureLegacyUserReadyForSession(db *sql.DB, legacyUser *models.User) (int64, error) {
desiredID := int64(legacyUser.ID)
_, err := db.Exec(`
INSERT INTO mk_dfusr (
id,
username,
email,
full_name,
mobile,
address,
is_active,
password_hash,
force_password_change,
created_at,
updated_at
)
VALUES (
$1,$2,$3,$4,$5,$6,$7,'',true,NOW(),NOW()
)
ON CONFLICT (id)
DO UPDATE SET
username = EXCLUDED.username,
email = EXCLUDED.email,
full_name = COALESCE(NULLIF(EXCLUDED.full_name, ''), mk_dfusr.full_name),
mobile = COALESCE(NULLIF(EXCLUDED.mobile, ''), mk_dfusr.mobile),
address = COALESCE(NULLIF(EXCLUDED.address, ''), mk_dfusr.address),
is_active = EXCLUDED.is_active,
force_password_change = true,
updated_at = NOW()
`,
desiredID,
strings.TrimSpace(legacyUser.Username),
strings.TrimSpace(legacyUser.Email),
strings.TrimSpace(legacyUser.FullName),
strings.TrimSpace(legacyUser.Mobile),
strings.TrimSpace(legacyUser.Address),
legacyUser.IsActive,
)
if err == nil {
return desiredID, nil
}
mkRepo := repository.NewMkUserRepository(db)
existing, lookupErr := mkRepo.GetByUsername(legacyUser.Username)
if lookupErr != nil {
return 0, err
}
_, updErr := db.Exec(`
UPDATE mk_dfusr
SET
is_active = $1,
force_password_change = true,
updated_at = NOW()
WHERE id = $2
`, legacyUser.IsActive, existing.ID)
if updErr != nil {
return 0, updErr
}
return existing.ID, nil
}
func LoginHandler(db *sql.DB) http.HandlerFunc { func LoginHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -84,20 +154,37 @@ func LoginHandler(db *sql.DB) http.HandlerFunc {
if err == nil { if err == nil {
// mk_dfusr authoritative // mk_dfusr authoritative
if strings.TrimSpace(mkUser.PasswordHash) != "" { mkHash := strings.TrimSpace(mkUser.PasswordHash)
if mkHash != "" {
if bcrypt.CompareHashAndPassword( if looksLikeBcryptHash(mkHash) {
[]byte(mkUser.PasswordHash), cmpErr := bcrypt.CompareHashAndPassword(
[]byte(mkHash),
[]byte(pass), []byte(pass),
) != nil { )
http.Error(w, "Kullanıcı adı veya parola hatalı", http.StatusUnauthorized) if cmpErr == nil {
return
}
_ = mkRepo.TouchLastLogin(mkUser.ID) _ = mkRepo.TouchLastLogin(mkUser.ID)
writeLoginResponse(w, db, mkUser) writeLoginResponse(w, db, mkUser)
return return
} }
if !mkUser.ForcePasswordChange {
http.Error(w, "invalid credentials", http.StatusUnauthorized)
return
}
log.Printf(
"LOGIN FALLBACK legacy allowed (force_password_change=true) username=%s id=%d",
mkUser.Username,
mkUser.ID,
)
} else {
log.Printf(
"LOGIN FALLBACK legacy allowed (non-bcrypt mk hash) username=%s id=%d",
mkUser.Username,
mkUser.ID,
)
}
}
// password_hash boşsa legacy fallback // password_hash boşsa legacy fallback
} else if err != repository.ErrMkUserNotFound { } else if err != repository.ErrMkUserNotFound {
log.Println("❌ mk_dfusr lookup error:", err) log.Println("❌ mk_dfusr lookup error:", err)
@@ -137,8 +224,15 @@ func LoginHandler(db *sql.DB) http.HandlerFunc {
// 3⃣ LEGACY SESSION (PENDING MIGRATION) // 3⃣ LEGACY SESSION (PENDING MIGRATION)
// - mk_dfusr migration is completed in /api/password/change // - mk_dfusr migration is completed in /api/password/change
// ================================================== // ==================================================
mkID, err := ensureLegacyUserReadyForSession(db, legacyUser)
if err != nil {
log.Printf("LEGACY LOGIN MIGRATION BIND FAILED username=%s err=%v", login, err)
http.Error(w, "Giriş yapılamadı", http.StatusInternalServerError)
return
}
mkUser = &models.MkUser{ mkUser = &models.MkUser{
ID: int64(legacyUser.ID), ID: mkID,
Username: legacyUser.Username, Username: legacyUser.Username,
Email: legacyUser.Email, Email: legacyUser.Email,
IsActive: legacyUser.IsActive, IsActive: legacyUser.IsActive,
@@ -150,7 +244,7 @@ func LoginHandler(db *sql.DB) http.HandlerFunc {
auditlog.Write(auditlog.ActivityLog{ auditlog.Write(auditlog.ActivityLog{
ActionType: "LEGACY_USER_LOGIN_PENDING_MIGRATION", ActionType: "LEGACY_USER_LOGIN_PENDING_MIGRATION",
ActionCategory: "security", ActionCategory: "security",
Description: "legacy login ok, first password change required", Description: "legacy giriş başarılı, ilk şifre değişikliği gerekli",
IsSuccess: true, IsSuccess: true,
}) })

View File

@@ -507,8 +507,8 @@ func getOrderLinesFromDB(db *sql.DB, orderID string) ([]OrderLineRaw, error) {
L.ItemDim1Code, L.ItemDim1Code,
L.ItemDim2Code, L.ItemDim2Code,
L.Qty1, L.Qty1,
L.Price, ISNULL(CD.Price, 0) AS Price,
L.DocCurrencyCode, ISNULL(CD.CurrencyCode, ISNULL(L.DocCurrencyCode, 'TRY')) AS DocCurrencyCode,
L.DeliveryDate, L.DeliveryDate,
L.LineDescription, L.LineDescription,
P.ProductAtt01Desc, P.ProductAtt01Desc,
@@ -521,6 +521,9 @@ func getOrderLinesFromDB(db *sql.DB, orderID string) ([]OrderLineRaw, error) {
L.VatCode, L.VatCode,
L.VatRate L.VatRate
FROM BAGGI_V3.dbo.trOrderLine AS L FROM BAGGI_V3.dbo.trOrderLine AS L
LEFT JOIN BAGGI_V3.dbo.trOrderLineCurrency AS CD WITH (NOLOCK)
ON CD.OrderLineID = L.OrderLineID
AND CD.CurrencyCode = ISNULL(NULLIF(LTRIM(RTRIM(L.DocCurrencyCode)), ''), 'TRY')
LEFT JOIN ProductFilterWithDescription('TR') AS P LEFT JOIN ProductFilterWithDescription('TR') AS P
ON LTRIM(RTRIM(P.ProductCode)) = LTRIM(RTRIM(L.ItemCode)) ON LTRIM(RTRIM(P.ProductCode)) = LTRIM(RTRIM(L.ItemCode))
WHERE L.OrderHeaderID = @p1 WHERE L.OrderHeaderID = @p1
@@ -777,8 +780,12 @@ func drawOrderHeader(pdf *gofpdf.Fpdf, h *OrderHeader, showDesc bool) float64 {
/* ---------------------------------------------------- /* ----------------------------------------------------
5) AÇIKLAMA (Varsa) 5) AÇIKLAMA (Varsa)
---------------------------------------------------- */ ---------------------------------------------------- */
if showDesc && strings.TrimSpace(h.Description) != "" { desc := strings.TrimSpace(h.Description)
text := strings.TrimSpace(h.Description) if desc == "" {
desc = strings.TrimSpace(h.InternalDesc)
}
if showDesc && desc != "" {
text := desc
pdf.SetFont("dejavu", "", 8) // wrapte kullanılacak font pdf.SetFont("dejavu", "", 8) // wrapte kullanılacak font
lineH := 4.0 lineH := 4.0

View File

@@ -58,7 +58,7 @@ func OrderListRoute(mssql *sql.DB) http.Handler {
count := 0 count := 0
// ================================================== // ==================================================
// 🧠 SCAN — SQL SELECT ile BİRE BİR (17 kolon) // 🧠 SCAN — SQL SELECT ile BİRE BİR (18 kolon)
// ================================================== // ==================================================
for rows.Next() { for rows.Next() {
@@ -85,9 +85,10 @@ func OrderListRoute(mssql *sql.DB) http.Handler {
&o.PackedRatePct, // 14 &o.PackedRatePct, // 14
&o.IsCreditableConfirmed, // 15 &o.IsCreditableConfirmed, // 15
&o.Description, // 16 &o.HasUretimUrunu, // 16
&o.Description, // 17
&o.ExchangeRateUSD, // 17 &o.ExchangeRateUSD, // 18
) )
if err != nil { if err != nil {

View File

@@ -0,0 +1,294 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/models"
"bssapp-backend/queries"
"database/sql"
"encoding/json"
"errors"
"log"
"net/http"
"strings"
"github.com/gorilla/mux"
)
// ======================================================
// 📌 OrderProductionItemsRoute — U ürün satırları
// ======================================================
func OrderProductionItemsRoute(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")
id := mux.Vars(r)["id"]
if id == "" {
http.Error(w, "OrderHeaderID bulunamadı", http.StatusBadRequest)
return
}
rows, err := queries.GetOrderProductionItems(mssql, id)
if err != nil {
log.Printf("❌ SQL sorgu hatası: %v", err)
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
return
}
defer rows.Close()
list := make([]models.OrderProductionItem, 0, 100)
for rows.Next() {
var o models.OrderProductionItem
if err := rows.Scan(
&o.OrderHeaderID,
&o.OrderLineID,
&o.ItemTypeCode,
&o.OldDim1,
&o.OldDim3,
&o.OldItemCode,
&o.OldColor,
&o.OldDim2,
&o.OldDesc,
&o.NewItemCode,
&o.NewColor,
&o.NewDim2,
&o.NewDesc,
&o.IsVariantMissing,
); err != nil {
log.Printf("⚠️ SCAN HATASI: %v", err)
continue
}
list = append(list, o)
}
if err := rows.Err(); err != nil {
log.Printf("⚠️ rows.Err(): %v", err)
}
if err := json.NewEncoder(w).Encode(list); err != nil {
log.Printf("❌ encode error: %v", err)
}
})
}
// ======================================================
// 📌 OrderProductionInsertMissingRoute — eksik varyantları ekler
// ======================================================
func OrderProductionInsertMissingRoute(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")
id := mux.Vars(r)["id"]
if id == "" {
http.Error(w, "OrderHeaderID bulunamadı", http.StatusBadRequest)
return
}
claims, _ := auth.GetClaimsFromContext(r.Context())
username := ""
if claims != nil {
username = claims.Username
}
if username == "" {
username = "system"
}
affected, err := queries.InsertMissingProductionVariants(mssql, id, username)
if err != nil {
log.Printf("❌ INSERT varyant hatası: %v", err)
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
return
}
resp := map[string]any{
"inserted": affected,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("❌ encode error: %v", err)
}
})
}
// ======================================================
// OrderProductionValidateRoute - yeni model varyant kontrolu
// ======================================================
func OrderProductionValidateRoute(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")
id := mux.Vars(r)["id"]
if id == "" {
http.Error(w, "OrderHeaderID bulunamadi", http.StatusBadRequest)
return
}
var payload models.OrderProductionUpdatePayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "Gecersiz istek", http.StatusBadRequest)
return
}
if err := validateUpdateLines(payload.Lines); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
missing, err := buildMissingVariants(mssql, id, payload.Lines)
if err != nil {
log.Printf("❌ validate error: %v", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
resp := map[string]any{
"missingCount": len(missing),
"missing": missing,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("❌ encode error: %v", err)
}
})
}
// ======================================================
// OrderProductionApplyRoute - yeni model varyant guncelleme
// ======================================================
func OrderProductionApplyRoute(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")
id := mux.Vars(r)["id"]
if id == "" {
http.Error(w, "OrderHeaderID bulunamadi", http.StatusBadRequest)
return
}
var payload models.OrderProductionUpdatePayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "Gecersiz istek", http.StatusBadRequest)
return
}
if err := validateUpdateLines(payload.Lines); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
missing, err := buildMissingVariants(mssql, id, payload.Lines)
if err != nil {
log.Printf("❌ apply validate error: %v", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
if len(missing) > 0 && !payload.InsertMissing {
w.WriteHeader(http.StatusConflict)
_ = json.NewEncoder(w).Encode(map[string]any{
"missingCount": len(missing),
"missing": missing,
"message": "Eksik varyantlar var",
})
return
}
claims, _ := auth.GetClaimsFromContext(r.Context())
username := ""
if claims != nil {
username = claims.Username
}
if strings.TrimSpace(username) == "" {
username = "system"
}
tx, err := mssql.Begin()
if err != nil {
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
defer tx.Rollback()
var inserted int64
if payload.InsertMissing {
inserted, err = queries.InsertMissingVariantsTx(tx, missing, username)
if err != nil {
log.Printf("❌ insert missing error: %v", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
}
updated, err := queries.UpdateOrderLinesTx(tx, id, payload.Lines, username)
if err != nil {
log.Printf("❌ update order lines error: %v", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
if err := tx.Commit(); err != nil {
log.Printf("❌ commit error: %v", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
resp := map[string]any{
"updated": updated,
"inserted": inserted,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("❌ encode error: %v", err)
}
})
}
func buildMissingVariants(mssql *sql.DB, orderHeaderID string, lines []models.OrderProductionUpdateLine) ([]models.OrderProductionMissingVariant, error) {
missing := make([]models.OrderProductionMissingVariant, 0)
for _, line := range lines {
lineID := strings.TrimSpace(line.OrderLineID)
newItem := strings.TrimSpace(line.NewItemCode)
newColor := strings.TrimSpace(line.NewColor)
newDim2 := strings.TrimSpace(line.NewDim2)
if lineID == "" || newItem == "" || newColor == "" {
continue
}
itemTypeCode, dim1, _, dim3, err := queries.GetOrderLineDims(mssql, orderHeaderID, lineID)
if err != nil {
return nil, err
}
exists, err := queries.VariantExists(mssql, itemTypeCode, newItem, newColor, dim1, newDim2, dim3)
if err != nil {
return nil, err
}
if !exists {
missing = append(missing, models.OrderProductionMissingVariant{
OrderLineID: lineID,
ItemTypeCode: itemTypeCode,
ItemCode: newItem,
ColorCode: newColor,
ItemDim1Code: dim1,
ItemDim2Code: newDim2,
ItemDim3Code: dim3,
})
}
}
return missing, nil
}
func validateUpdateLines(lines []models.OrderProductionUpdateLine) error {
for _, line := range lines {
if strings.TrimSpace(line.OrderLineID) == "" {
return errors.New("OrderLineID zorunlu")
}
if strings.TrimSpace(line.NewItemCode) == "" {
return errors.New("Yeni urun kodu zorunlu")
}
if strings.TrimSpace(line.NewColor) == "" {
return errors.New("Yeni renk kodu zorunlu")
}
}
return nil
}

View File

@@ -0,0 +1,130 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/db"
"bssapp-backend/models"
"bssapp-backend/queries"
"database/sql"
"encoding/json"
"log"
"net/http"
"strings"
)
// ======================================================
// 📌 OrderProductionListRoute — Üretime verilecek siparişler
// ======================================================
func OrderProductionListRoute(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/production-list search raw=%q trimmed=%q lenRaw=%d lenTrim=%d",
raw,
search,
len(raw),
len(search),
)
// --------------------------------------------------
// 🗄 SQL CALL (WITH CONTEXT)
// --------------------------------------------------
rows, err := queries.GetOrderProductionList(
r.Context(),
mssql,
db.PgDB,
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 (18 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.PackedAmount, // 12
&o.PackedUSD, // 13
&o.PackedRatePct, // 14
&o.IsCreditableConfirmed, // 15
&o.HasUretimUrunu, // 16
&o.Description, // 17
&o.ExchangeRateUSD, // 18
)
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 production list DONE | user=%d | search=%q | resultCount=%d",
claims.ID,
search,
count,
)
// --------------------------------------------------
// ✅ JSON RESPONSE
// --------------------------------------------------
if err := json.NewEncoder(w).Encode(list); err != nil {
log.Printf("❌ encode error: %v", err)
}
})
}

View File

@@ -0,0 +1,230 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/db"
"database/sql"
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"github.com/gorilla/mux"
)
type ProductionUpdateLine struct {
OrderLineID string `json:"OrderLineID"`
ItemTypeCode int16 `json:"ItemTypeCode"`
ItemCode string `json:"ItemCode"`
ColorCode string `json:"ColorCode"`
ItemDim1Code string `json:"ItemDim1Code"`
ItemDim2Code string `json:"ItemDim2Code"`
ItemDim3Code string `json:"ItemDim3Code"`
LineDescription string `json:"LineDescription"`
}
type ProductionUpdateRequest struct {
Lines []ProductionUpdateLine `json:"lines"`
InsertMissing bool `json:"insertMissing"`
}
type MissingVariant struct {
ItemTypeCode int16 `json:"ItemTypeCode"`
ItemCode string `json:"ItemCode"`
ColorCode string `json:"ColorCode"`
ItemDim1Code string `json:"ItemDim1Code"`
ItemDim2Code string `json:"ItemDim2Code"`
ItemDim3Code string `json:"ItemDim3Code"`
}
// ======================================================
// 📌 OrderProductionUpdateRoute — U ürün satırlarını güncelle
// ======================================================
func OrderProductionUpdateRoute(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")
id := mux.Vars(r)["id"]
if id == "" {
http.Error(w, "OrderHeaderID bulunamadı", http.StatusBadRequest)
return
}
var req ProductionUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Geçersiz JSON", http.StatusBadRequest)
return
}
if len(req.Lines) == 0 {
http.Error(w, "Satır bulunamadı", http.StatusBadRequest)
return
}
claims, _ := auth.GetClaimsFromContext(r.Context())
username := ""
if claims != nil {
username = claims.Username
}
if username == "" {
username = "system"
}
tx, err := db.MssqlDB.Begin()
if err != nil {
http.Error(w, "İşlem başlatılamadı", http.StatusInternalServerError)
return
}
defer tx.Rollback()
// 1) Eksik varyantları kontrol et
missingMap := make(map[string]MissingVariant)
checkStmt, err := tx.Prepare(`
SELECT TOP 1 1
FROM dbo.prItemVariant
WHERE ItemTypeCode = @p1
AND ItemCode = @p2
AND ColorCode = @p3
AND ISNULL(ItemDim1Code,'') = ISNULL(@p4,'')
AND ISNULL(ItemDim2Code,'') = ISNULL(@p5,'')
AND ISNULL(ItemDim3Code,'') = ISNULL(@p6,'')
`)
if err != nil {
http.Error(w, "Varyant kontrolü hazırlanamadı", http.StatusInternalServerError)
return
}
defer checkStmt.Close()
for _, ln := range req.Lines {
if strings.TrimSpace(ln.ItemCode) == "" {
http.Error(w, "Yeni model kodu boş", http.StatusBadRequest)
return
}
row := checkStmt.QueryRow(
ln.ItemTypeCode,
ln.ItemCode,
ln.ColorCode,
ln.ItemDim1Code,
ln.ItemDim2Code,
ln.ItemDim3Code,
)
var ok int
if err := row.Scan(&ok); err != nil {
if errors.Is(err, sql.ErrNoRows) {
key := strings.Join([]string{
ln.ItemCode, ln.ColorCode, ln.ItemDim1Code, ln.ItemDim2Code, ln.ItemDim3Code,
}, "|")
missingMap[key] = MissingVariant{
ItemTypeCode: ln.ItemTypeCode,
ItemCode: ln.ItemCode,
ColorCode: ln.ColorCode,
ItemDim1Code: ln.ItemDim1Code,
ItemDim2Code: ln.ItemDim2Code,
ItemDim3Code: ln.ItemDim3Code,
}
continue
}
http.Error(w, "Varyant kontrolü hatası", http.StatusInternalServerError)
return
}
}
if len(missingMap) > 0 && !req.InsertMissing {
missing := make([]MissingVariant, 0, len(missingMap))
for _, v := range missingMap {
missing = append(missing, v)
}
w.WriteHeader(http.StatusConflict)
_ = json.NewEncoder(w).Encode(map[string]any{
"missing": missing,
})
return
}
// 2) Eksikleri ekle (gerekirse)
if len(missingMap) > 0 {
// PLU üretimi (max + row_number)
var basePlu int64
if err := tx.QueryRow(`SELECT ISNULL(MAX(PLU),0) FROM dbo.prItemVariant WITH (UPDLOCK, HOLDLOCK)`).Scan(&basePlu); err != nil {
http.Error(w, "PLU alınamadı", http.StatusInternalServerError)
return
}
now := time.Now()
i := int64(0)
for _, v := range missingMap {
i++
if _, err := tx.Exec(`
INSERT INTO dbo.prItemVariant
(
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code,
PLU, CreatedUserName, CreatedDate, LastUpdatedUserName, LastUpdatedDate
)
VALUES
(@p1,@p2,@p3,@p4,@p5,@p6,@p7,@p8,@p9,@p8,@p9)
`,
v.ItemTypeCode,
v.ItemCode,
v.ColorCode,
v.ItemDim1Code,
v.ItemDim2Code,
v.ItemDim3Code,
basePlu+i,
username,
now,
); err != nil {
http.Error(w, "Varyant insert hatası", http.StatusInternalServerError)
return
}
}
}
// 3) trOrderLine güncelle
updStmt, err := tx.Prepare(`
UPDATE dbo.trOrderLine
SET
ItemCode = @p1,
ColorCode = @p2,
ItemDim2Code = @p3,
LineDescription = @p4,
LastUpdatedUserName = @p5,
LastUpdatedDate = @p6
WHERE OrderHeaderID = @p7
AND OrderLineID = @p8
`)
if err != nil {
http.Error(w, "Update hazırlığı başarısız", http.StatusInternalServerError)
return
}
defer updStmt.Close()
now := time.Now()
for _, ln := range req.Lines {
if _, err := updStmt.Exec(
ln.ItemCode,
ln.ColorCode,
ln.ItemDim2Code,
ln.LineDescription,
username,
now,
id,
ln.OrderLineID,
); err != nil {
http.Error(w, "Satır güncelleme hatası", http.StatusInternalServerError)
return
}
}
if err := tx.Commit(); err != nil {
http.Error(w, "Commit hatası", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(map[string]any{
"status": "ok",
"updated": len(req.Lines),
})
})
}

View File

@@ -6,9 +6,7 @@ import (
"bssapp-backend/internal/security" "bssapp-backend/internal/security"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"os"
"strings" "strings"
"time" "time"
) )
@@ -75,7 +73,7 @@ func ForgotPasswordHandler(
_, _ = db.Exec(` _, _ = db.Exec(`
INSERT INTO dfusr_password_reset ( INSERT INTO dfusr_password_reset (
dfusr_id, dfusr_id,
token, token_hash,
expires_at expires_at
) )
VALUES ($1, $2, $3) VALUES ($1, $2, $3)
@@ -84,11 +82,7 @@ func ForgotPasswordHandler(
// ------------------------------------------------------- // -------------------------------------------------------
// 5⃣ Reset URL (PLAIN token) // 5⃣ Reset URL (PLAIN token)
// ------------------------------------------------------- // -------------------------------------------------------
resetURL := fmt.Sprintf( resetURL := security.BuildResetURL(plain)
"%s/password-reset/%s",
os.Getenv("FRONTEND_URL"),
plain,
)
// ------------------------------------------------------- // -------------------------------------------------------
// 6⃣ Mail gönder (fail olsa bile enumeration yok) // 6⃣ Mail gönder (fail olsa bile enumeration yok)

View File

@@ -43,9 +43,9 @@ func CompletePasswordResetHandler(db *sql.DB) http.HandlerFunc {
var expiresAt time.Time var expiresAt time.Time
err := db.QueryRow(` err := db.QueryRow(`
SELECT mk_dfusr_id, expires_at SELECT dfusr_id, expires_at
FROM mk_dfusr_password_reset FROM dfusr_password_reset
WHERE token = $1 WHERE token_hash = $1
AND used_at IS NULL AND used_at IS NULL
`, tokenHash).Scan(&userID, &expiresAt) `, tokenHash).Scan(&userID, &expiresAt)
@@ -84,9 +84,10 @@ func CompletePasswordResetHandler(db *sql.DB) http.HandlerFunc {
// token tüket // token tüket
if _, err := tx.Exec(` if _, err := tx.Exec(`
UPDATE mk_dfusr_password_reset UPDATE dfusr_password_reset
SET used_at = now() SET used_at = now()
WHERE token = $1 WHERE token_hash = $1
AND used_at IS NULL
`, tokenHash); err != nil { `, tokenHash); err != nil {
http.Error(w, "token update failed", http.StatusInternalServerError) http.Error(w, "token update failed", http.StatusInternalServerError)
return return

View File

@@ -1,9 +1,8 @@
package routes package routes
import ( import (
"crypto/sha256" "bssapp-backend/internal/security"
"database/sql" "database/sql"
"encoding/hex"
"net/http" "net/http"
"time" "time"
@@ -21,8 +20,7 @@ func ValidatePasswordResetTokenHandler(db *sql.DB) http.HandlerFunc {
} }
// 🔐 plain token -> hash // 🔐 plain token -> hash
h := sha256.Sum256([]byte(token)) tokenHash := security.HashToken(token)
tokenHash := hex.EncodeToString(h[:])
var ( var (
userID int64 userID int64
@@ -31,8 +29,8 @@ func ValidatePasswordResetTokenHandler(db *sql.DB) http.HandlerFunc {
) )
err := db.QueryRow(` err := db.QueryRow(`
SELECT user_id, expires_at, used_at SELECT dfusr_id, expires_at, used_at
FROM password_reset_tokens FROM dfusr_password_reset
WHERE token_hash = $1 WHERE token_hash = $1
LIMIT 1 LIMIT 1
`, tokenHash).Scan(&userID, &expiresAt, &usedAt) `, tokenHash).Scan(&userID, &expiresAt, &usedAt)

View File

@@ -14,6 +14,7 @@ import (
"strings" "strings"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/lib/pq"
) )
type IdTitleOption struct { type IdTitleOption struct {
@@ -57,7 +58,7 @@ type RoleDepartmentPermissionHandler struct {
func NewRoleDepartmentPermissionHandler(db *sql.DB) *RoleDepartmentPermissionHandler { func NewRoleDepartmentPermissionHandler(db *sql.DB) *RoleDepartmentPermissionHandler {
return &RoleDepartmentPermissionHandler{ return &RoleDepartmentPermissionHandler{
DB: db, // ✅ EKLENDİ DB: db, // Added
Repo: permissions.NewRoleDepartmentPermissionRepo(db), Repo: permissions.NewRoleDepartmentPermissionRepo(db),
} }
} }
@@ -417,7 +418,7 @@ func (h *PermissionHandler) GetUserOverrides(w http.ResponseWriter, r *http.Requ
list, err := h.Repo.GetUserOverridesByUserID(userID) list, err := h.Repo.GetUserOverridesByUserID(userID)
if err != nil { if err != nil {
log.Println("USER OVERRIDE LOAD ERROR:", err) log.Println("USER OVERRIDE LOAD ERROR:", err)
http.Error(w, "db error", http.StatusInternalServerError) http.Error(w, "db error", http.StatusInternalServerError)
return return
} }
@@ -425,6 +426,138 @@ func (h *PermissionHandler) GetUserOverrides(w http.ResponseWriter, r *http.Requ
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(list) _ = json.NewEncoder(w).Encode(list)
} }
type routePermissionSeed struct {
Module string
Action string
Path string
}
type moduleActionSeed struct {
Module string
Action string
}
type permissionSnapshot struct {
user map[string]bool
roleDept map[string]bool
role map[string]bool
}
func permissionKey(module, action string) string {
return module + "|" + action
}
func loadPermissionSnapshot(
db *sql.DB,
userID int64,
roleID int64,
deptCodes []string,
) (permissionSnapshot, error) {
snapshot := permissionSnapshot{
user: make(map[string]bool, 128),
roleDept: make(map[string]bool, 128),
role: make(map[string]bool, 128),
}
userRows, err := db.Query(`
SELECT module_code, action, allowed
FROM mk_sys_user_permissions
WHERE user_id = $1
`, userID)
if err != nil {
return snapshot, err
}
for userRows.Next() {
var module, action string
var allowed bool
if err := userRows.Scan(&module, &action, &allowed); err != nil {
_ = userRows.Close()
return snapshot, err
}
snapshot.user[permissionKey(module, action)] = allowed
}
if err := userRows.Err(); err != nil {
_ = userRows.Close()
return snapshot, err
}
_ = userRows.Close()
if len(deptCodes) > 0 {
roleDeptRows, err := db.Query(`
SELECT module_code, action, BOOL_OR(allowed) AS allowed
FROM vw_role_dept_permissions
WHERE role_id = $1
AND department_code = ANY($2)
GROUP BY module_code, action
`,
roleID,
pq.Array(deptCodes),
)
if err != nil {
return snapshot, err
}
for roleDeptRows.Next() {
var module, action string
var allowed bool
if err := roleDeptRows.Scan(&module, &action, &allowed); err != nil {
_ = roleDeptRows.Close()
return snapshot, err
}
snapshot.roleDept[permissionKey(module, action)] = allowed
}
if err := roleDeptRows.Err(); err != nil {
_ = roleDeptRows.Close()
return snapshot, err
}
_ = roleDeptRows.Close()
}
roleRows, err := db.Query(`
SELECT module_code, action, allowed
FROM mk_sys_role_permissions
WHERE role_id = $1
`, roleID)
if err != nil {
return snapshot, err
}
for roleRows.Next() {
var module, action string
var allowed bool
if err := roleRows.Scan(&module, &action, &allowed); err != nil {
_ = roleRows.Close()
return snapshot, err
}
snapshot.role[permissionKey(module, action)] = allowed
}
if err := roleRows.Err(); err != nil {
_ = roleRows.Close()
return snapshot, err
}
_ = roleRows.Close()
return snapshot, nil
}
func resolvePermissionFromSnapshot(
s permissionSnapshot,
module string,
action string,
) bool {
key := permissionKey(module, action)
if allowed, ok := s.user[key]; ok {
return allowed
}
if allowed, ok := s.roleDept[key]; ok {
return allowed
}
if allowed, ok := s.role[key]; ok {
return allowed
}
return false
}
func GetUserRoutePermissionsHandler(db *sql.DB) http.HandlerFunc { func GetUserRoutePermissionsHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -436,10 +569,16 @@ func GetUserRoutePermissionsHandler(db *sql.DB) http.HandlerFunc {
return return
} }
repo := permissions.NewPermissionRepository(db) snapshot, err := loadPermissionSnapshot(
db,
// JWTden departmanlar int64(claims.ID),
depts := claims.DepartmentCodes int64(claims.RoleID),
claims.DepartmentCodes,
)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
rows, err := db.Query(` rows, err := db.Query(`
SELECT DISTINCT SELECT DISTINCT
@@ -454,17 +593,9 @@ func GetUserRoutePermissionsHandler(db *sql.DB) http.HandlerFunc {
} }
defer rows.Close() defer rows.Close()
type Row struct { routeSeeds := make([]routePermissionSeed, 0, 128)
Route string `json:"route"`
CanAccess bool `json:"can_access"`
}
list := make([]Row, 0, 64)
for rows.Next() { for rows.Next() {
var module, action, path string var module, action, path string
if err := rows.Scan( if err := rows.Scan(
&module, &module,
&action, &action,
@@ -473,22 +604,26 @@ func GetUserRoutePermissionsHandler(db *sql.DB) http.HandlerFunc {
continue continue
} }
allowed, err := repo.ResolvePermissionChain( routeSeeds = append(routeSeeds, routePermissionSeed{
int64(claims.ID), Module: module,
int64(claims.RoleID), Action: action,
depts, Path: path,
module, })
action, }
) if err := rows.Err(); err != nil {
http.Error(w, err.Error(), 500)
if err != nil { return
log.Println("PERM RESOLVE ERROR:", err)
continue
} }
list := make([]Row, 0, len(routeSeeds))
for _, route := range routeSeeds {
list = append(list, Row{ list = append(list, Row{
Route: path, Route: route.Path,
CanAccess: allowed, CanAccess: resolvePermissionFromSnapshot(
snapshot,
route.Module,
route.Action,
),
}) })
} }
@@ -507,18 +642,17 @@ func GetMyEffectivePermissions(db *sql.DB) http.HandlerFunc {
return return
} }
repo := permissions.NewPermissionRepository(db) snapshot, err := loadPermissionSnapshot(
db,
// ✅ JWT'DEN DEPARTMENTS int64(claims.ID),
depts := claims.DepartmentCodes int64(claims.RoleID),
claims.DepartmentCodes,
log.Printf("🧪 EFFECTIVE PERM | user=%d role=%d depts=%v",
claims.ID,
claims.RoleID,
depts,
) )
if err != nil {
http.Error(w, err.Error(), 500)
return
}
// all system perms
all, err := db.Query(` all, err := db.Query(`
SELECT DISTINCT module_code, action SELECT DISTINCT module_code, action
FROM mk_sys_routes FROM mk_sys_routes
@@ -529,14 +663,7 @@ func GetMyEffectivePermissions(db *sql.DB) http.HandlerFunc {
} }
defer all.Close() defer all.Close()
type Row struct { moduleActions := make([]moduleActionSeed, 0, 128)
Module string `json:"module"`
Action string `json:"action"`
Allowed bool `json:"allowed"`
}
list := make([]Row, 0, 128)
for all.Next() { for all.Next() {
var m, a string var m, a string
@@ -544,22 +671,32 @@ func GetMyEffectivePermissions(db *sql.DB) http.HandlerFunc {
continue continue
} }
allowed, err := repo.ResolvePermissionChain( moduleActions = append(moduleActions, moduleActionSeed{
int64(claims.ID),
int64(claims.RoleID),
depts,
m,
a,
)
if err != nil {
continue
}
list = append(list, Row{
Module: m, Module: m,
Action: a, Action: a,
Allowed: allowed, })
}
if err := all.Err(); err != nil {
http.Error(w, err.Error(), 500)
return
}
type Row struct {
Module string `json:"module"`
Action string `json:"action"`
Allowed bool `json:"allowed"`
}
list := make([]Row, 0, len(moduleActions))
for _, item := range moduleActions {
list = append(list, Row{
Module: item.Module,
Action: item.Action,
Allowed: resolvePermissionFromSnapshot(
snapshot,
item.Module,
item.Action,
),
}) })
} }

View File

@@ -14,12 +14,12 @@ import (
"io" "io"
"log" "log"
"net/http" "net/http"
"os"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/lib/pq"
) )
// ====================================================== // ======================================================
@@ -374,12 +374,42 @@ func handleUserDelete(db *sql.DB, w http.ResponseWriter, r *http.Request, userID
`DELETE FROM dfusr_nebim_user WHERE dfusr_id = $1`, `DELETE FROM dfusr_nebim_user WHERE dfusr_id = $1`,
} }
for _, q := range cleanupQueries { isUndefinedTable := func(err error) bool {
if err == nil {
return false
}
if pe, ok := err.(*pq.Error); ok {
return pe.Code == "42P01" // undefined_table
}
return false
}
for i, q := range cleanupQueries {
sp := fmt.Sprintf("sp_user_cleanup_%d", i)
_, _ = tx.Exec("SAVEPOINT " + sp)
if _, err := tx.Exec(q, userID); err != nil { if _, err := tx.Exec(q, userID); err != nil {
// rollback the failed statement so tx can continue
_, _ = tx.Exec("ROLLBACK TO SAVEPOINT " + sp)
if isUndefinedTable(err) {
log.Printf("⚠️ [UserDetail] cleanup skipped (table missing) user_id=%d query=%s", userID, q)
_, _ = tx.Exec("RELEASE SAVEPOINT " + sp)
continue
}
if pe, ok := err.(*pq.Error); ok {
log.Printf(
"❌ [UserDetail] cleanup failed user_id=%d code=%s detail=%s constraint=%s table=%s err=%v query=%s",
userID, pe.Code, pe.Detail, pe.Constraint, pe.Table, err, q,
)
} else {
log.Printf("❌ [UserDetail] cleanup failed user_id=%d err=%v query=%s", userID, err, q) log.Printf("❌ [UserDetail] cleanup failed user_id=%d err=%v query=%s", userID, err, q)
}
http.Error(w, "Kullanici baglantilari silinemedi", http.StatusInternalServerError) http.Error(w, "Kullanici baglantilari silinemedi", http.StatusInternalServerError)
return return
} }
_, _ = tx.Exec("RELEASE SAVEPOINT " + sp)
} }
if _, err := tx.Exec(`DELETE FROM mk_dfusr WHERE id = $1`, userID); err != nil { if _, err := tx.Exec(`DELETE FROM mk_dfusr WHERE id = $1`, userID); err != nil {
@@ -388,12 +418,6 @@ func handleUserDelete(db *sql.DB, w http.ResponseWriter, r *http.Request, userID
return return
} }
if _, err := tx.Exec(`DELETE FROM dfusr WHERE id = $1`, userID); err != nil {
log.Printf("❌ [UserDetail] delete dfusr failed user_id=%d err=%v", userID, err)
http.Error(w, "Kullanici silinemedi", http.StatusInternalServerError)
return
}
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
log.Printf("❌ [UserDetail] delete commit failed user_id=%d err=%v", userID, err) log.Printf("❌ [UserDetail] delete commit failed user_id=%d err=%v", userID, err)
http.Error(w, "Commit basarisiz", http.StatusInternalServerError) http.Error(w, "Commit basarisiz", http.StatusInternalServerError)
@@ -405,7 +429,7 @@ func handleUserDelete(db *sql.DB, w http.ResponseWriter, r *http.Request, userID
ActionType: "user_delete", ActionType: "user_delete",
ActionCategory: "user_admin", ActionCategory: "user_admin",
ActionTarget: fmt.Sprintf("/api/users/%d", userID), ActionTarget: fmt.Sprintf("/api/users/%d", userID),
Description: "user deleted from mk_dfusr and dfusr", Description: "user deleted from mk_dfusr (dfusr retained)",
Username: claims.Username, Username: claims.Username,
RoleCode: claims.RoleCode, RoleCode: claims.RoleCode,
DfUsrID: int64(claims.ID), DfUsrID: int64(claims.ID),
@@ -457,16 +481,12 @@ func SendPasswordResetMailHandler(
// 💾 DB → SADECE HASH // 💾 DB → SADECE HASH
_, _ = db.Exec(` _, _ = db.Exec(`
INSERT INTO dfusr_password_reset (dfusr_id, token, expires_at) INSERT INTO dfusr_password_reset (dfusr_id, token_hash, expires_at)
VALUES ($1,$2,$3) VALUES ($1,$2,$3)
`, userID, hash, expires) `, userID, hash, expires)
// 🔗 URL → PLAIN // 🔗 URL → PLAIN
resetURL := fmt.Sprintf( resetURL := security.BuildResetURL(plain)
"%s/password-reset/%s",
os.Getenv("FRONTEND_URL"),
plain,
)
_ = mailer.SendPasswordResetMail(email, resetURL) _ = mailer.SendPasswordResetMail(email, resetURL)

View File

@@ -2,6 +2,10 @@ package services
import ( import (
"bssapp-backend/models" "bssapp-backend/models"
"crypto/md5"
"crypto/sha1"
"encoding/hex"
"log"
"strings" "strings"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@@ -16,7 +20,6 @@ func CheckPasswordWithLegacy(user *models.User, plain string) bool {
return false return false
} }
plain = strings.TrimSpace(plain)
if plain == "" { if plain == "" {
return false return false
} }
@@ -28,11 +31,100 @@ func CheckPasswordWithLegacy(user *models.User, plain string) bool {
// 1⃣ bcrypt hash mi? // 1⃣ bcrypt hash mi?
if isBcryptHash(stored) { if isBcryptHash(stored) {
return bcrypt.CompareHashAndPassword([]byte(stored), []byte(plain)) == nil candidates := make([]string, 0, 10)
seen := map[string]struct{}{}
add := func(v string) {
if v == "" {
return
}
if _, ok := seen[v]; ok {
return
}
seen[v] = struct{}{}
candidates = append(candidates, v)
}
add(plain)
trimmed := strings.TrimSpace(plain)
add(trimmed)
bases := append([]string(nil), candidates...)
for _, base := range bases {
md5Sum := md5.Sum([]byte(base))
md5Hex := hex.EncodeToString(md5Sum[:])
add(md5Hex)
add(strings.ToUpper(md5Hex))
sha1Sum := sha1.Sum([]byte(base))
sha1Hex := hex.EncodeToString(sha1Sum[:])
add(sha1Hex)
add(strings.ToUpper(sha1Hex))
}
var lastErr error
for _, candidate := range candidates {
if err := bcrypt.CompareHashAndPassword([]byte(stored), []byte(candidate)); err == nil {
return true
} else {
lastErr = err
}
if encoded, ok := encodeLegacySingleByte(candidate); ok {
if err := bcrypt.CompareHashAndPassword([]byte(stored), encoded); err == nil {
return true
} else {
lastErr = err
}
}
}
if lastErr != nil {
log.Printf(
"LEGACY BCRYPT MISMATCH stored_len=%d candidates=%d last_err=%v",
len(stored),
len(candidates),
lastErr,
)
}
return false
} }
// 2⃣ TAM LEGACY — düz metin (eski kayıtlar) // 2⃣ TAM LEGACY — düz metin (eski kayıtlar)
return stored == plain if stored == plain {
return true
}
trimmed := strings.TrimSpace(plain)
if trimmed != plain && trimmed != "" && stored == trimmed {
return true
}
// 3⃣ Legacy hash variants seen in old dfusr.upass data.
if isHexDigest(stored, 32) {
sumRaw := md5.Sum([]byte(plain))
if strings.EqualFold(stored, hex.EncodeToString(sumRaw[:])) {
return true
}
if trimmed != plain && trimmed != "" {
sumTrim := md5.Sum([]byte(trimmed))
if strings.EqualFold(stored, hex.EncodeToString(sumTrim[:])) {
return true
}
}
}
if isHexDigest(stored, 40) {
sumRaw := sha1.Sum([]byte(plain))
if strings.EqualFold(stored, hex.EncodeToString(sumRaw[:])) {
return true
}
if trimmed != plain && trimmed != "" {
sumTrim := sha1.Sum([]byte(trimmed))
if strings.EqualFold(stored, hex.EncodeToString(sumTrim[:])) {
return true
}
}
}
return false
} }
func isBcryptHash(s string) bool { func isBcryptHash(s string) bool {
@@ -40,3 +132,46 @@ func isBcryptHash(s string) bool {
strings.HasPrefix(s, "$2b$") || strings.HasPrefix(s, "$2b$") ||
strings.HasPrefix(s, "$2y$") strings.HasPrefix(s, "$2y$")
} }
func isHexDigest(s string, expectedLen int) bool {
if len(s) != expectedLen {
return false
}
for _, r := range s {
if (r < '0' || r > '9') &&
(r < 'a' || r > 'f') &&
(r < 'A' || r > 'F') {
return false
}
}
return true
}
// encodeLegacySingleByte converts text to a Turkish-compatible single-byte
// representation (similar to Windows-1254 / ISO-8859-9) for legacy bcrypt data.
func encodeLegacySingleByte(s string) ([]byte, bool) {
out := make([]byte, 0, len(s))
for _, r := range s {
switch r {
case 'Ğ':
out = append(out, 0xD0)
case 'ğ':
out = append(out, 0xF0)
case 'İ':
out = append(out, 0xDD)
case 'ı':
out = append(out, 0xFD)
case 'Ş':
out = append(out, 0xDE)
case 'ş':
out = append(out, 0xFE)
default:
if r >= 0 && r <= 0xFF {
out = append(out, byte(r))
} else {
return nil, false
}
}
}
return out, true
}

View File

@@ -1 +1 @@
VITE_API_BASE_URL=http://localhost:8080 VITE_API_BASE_URL=http://localhost:8080/api

View File

@@ -146,8 +146,6 @@ createQuasarApp(createApp, quasarUserOptions)
return Promise[ method ]([ return Promise[ method ]([
import(/* webpackMode: "eager" */ 'boot/axios'),
import(/* webpackMode: "eager" */ 'boot/dayjs') import(/* webpackMode: "eager" */ 'boot/dayjs')
]).then(bootFiles => { ]).then(bootFiles => {

View File

@@ -1,75 +0,0 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import { Quasar } from 'quasar'
import { markRaw } from 'vue'
import RootComponent from 'app/src/App.vue'
import createStore from 'app/src/stores/index'
import createRouter from 'app/src/router/index'
export default async function (createAppFn, quasarUserOptions) {
// Create the app instance.
// Here we inject into it the Quasar UI, the router & possibly the store.
const app = createAppFn(RootComponent)
app.use(Quasar, quasarUserOptions)
const store = typeof createStore === 'function'
? await createStore({})
: createStore
app.use(store)
const router = markRaw(
typeof createRouter === 'function'
? await createRouter({store})
: createRouter
)
// make router instance available in store
store.use(({ store }) => { store.router = router })
// Expose the app, the router and the store.
// Note that we are not mounting the app here, since bootstrapping will be
// different depending on whether we are in a browser or on the server.
return {
app,
store,
router
}
}

View File

@@ -1,154 +0,0 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import { createApp } from 'vue'
import '@quasar/extras/roboto-font/roboto-font.css'
import '@quasar/extras/material-icons/material-icons.css'
// We load Quasar stylesheet file
import 'quasar/dist/quasar.sass'
import 'src/css/app.css'
import createQuasarApp from './app.js'
import quasarUserOptions from './quasar-user-options.js'
const publicPath = `/`
async function start ({
app,
router
, store
}, bootFiles) {
let hasRedirected = false
const getRedirectUrl = url => {
try { return router.resolve(url).href }
catch (err) {}
return Object(url) === url
? null
: url
}
const redirect = url => {
hasRedirected = true
if (typeof url === 'string' && /^https?:\/\//.test(url)) {
window.location.href = url
return
}
const href = getRedirectUrl(url)
// continue if we didn't fail to resolve the url
if (href !== null) {
window.location.href = href
window.location.reload()
}
}
const urlPath = window.location.href.replace(window.location.origin, '')
for (let i = 0; hasRedirected === false && i < bootFiles.length; i++) {
try {
await bootFiles[i]({
app,
router,
store,
ssrContext: null,
redirect,
urlPath,
publicPath
})
}
catch (err) {
if (err && err.url) {
redirect(err.url)
return
}
console.error('[Quasar] boot error:', err)
return
}
}
if (hasRedirected === true) return
app.use(router)
app.mount('#q-app')
}
createQuasarApp(createApp, quasarUserOptions)
.then(app => {
// eventually remove this when Cordova/Capacitor/Electron support becomes old
const [ method, mapFn ] = Promise.allSettled !== void 0
? [
'allSettled',
bootFiles => bootFiles.map(result => {
if (result.status === 'rejected') {
console.error('[Quasar] boot error:', result.reason)
return
}
return result.value.default
})
]
: [
'all',
bootFiles => bootFiles.map(entry => entry.default)
]
return Promise[ method ]([
import(/* webpackMode: "eager" */ 'boot/dayjs')
]).then(bootFiles => {
const boot = mapFn(bootFiles).filter(entry => typeof entry === 'function')
start(app, boot)
})
})

View File

@@ -1,116 +0,0 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import App from 'app/src/App.vue'
let appPrefetch = typeof App.preFetch === 'function'
? App.preFetch
: (
// Class components return the component options (and the preFetch hook) inside __c property
App.__c !== void 0 && typeof App.__c.preFetch === 'function'
? App.__c.preFetch
: false
)
function getMatchedComponents (to, router) {
const route = to
? (to.matched ? to : router.resolve(to).route)
: router.currentRoute.value
if (!route) { return [] }
const matched = route.matched.filter(m => m.components !== void 0)
if (matched.length === 0) { return [] }
return Array.prototype.concat.apply([], matched.map(m => {
return Object.keys(m.components).map(key => {
const comp = m.components[key]
return {
path: m.path,
c: comp
}
})
}))
}
export function addPreFetchHooks ({ router, store, publicPath }) {
// Add router hook for handling preFetch.
// Doing it after initial route is resolved so that we don't double-fetch
// the data that we already have. Using router.beforeResolve() so that all
// async components are resolved.
router.beforeResolve((to, from, next) => {
const
urlPath = window.location.href.replace(window.location.origin, ''),
matched = getMatchedComponents(to, router),
prevMatched = getMatchedComponents(from, router)
let diffed = false
const preFetchList = matched
.filter((m, i) => {
return diffed || (diffed = (
!prevMatched[i] ||
prevMatched[i].c !== m.c ||
m.path.indexOf('/:') > -1 // does it has params?
))
})
.filter(m => m.c !== void 0 && (
typeof m.c.preFetch === 'function'
// Class components return the component options (and the preFetch hook) inside __c property
|| (m.c.__c !== void 0 && typeof m.c.__c.preFetch === 'function')
))
.map(m => m.c.__c !== void 0 ? m.c.__c.preFetch : m.c.preFetch)
if (appPrefetch !== false) {
preFetchList.unshift(appPrefetch)
appPrefetch = false
}
if (preFetchList.length === 0) {
return next()
}
let hasRedirected = false
const redirect = url => {
hasRedirected = true
next(url)
}
const proceed = () => {
if (hasRedirected === false) { next() }
}
preFetchList.reduce(
(promise, preFetch) => promise.then(() => hasRedirected === false && preFetch({
store,
currentRoute: to,
previousRoute: from,
redirect,
urlPath,
publicPath
})),
Promise.resolve()
)
.then(proceed)
.catch(e => {
console.error(e)
proceed()
})
})
}

View File

@@ -1,23 +0,0 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import lang from 'quasar/lang/tr.js'
import {Loading,Dialog,Notify} from 'quasar'
export default { config: {"notify":{"position":"top","timeout":2500}},lang,plugins: {Loading,Dialog,Notify} }

View File

@@ -1 +0,0 @@
.ol-page[data-v-68dfbebc]{padding:10px}.ol-filter-bar[data-v-68dfbebc]{margin-bottom:8px}.ol-filter-row[data-v-68dfbebc]{align-items:center;display:flex;flex-wrap:nowrap;gap:10px}.ol-filter-input[data-v-68dfbebc]{flex:0 0 136px;min-width:118px;width:136px}.ol-search[data-v-68dfbebc]{flex:1 1 360px;max-width:420px;min-width:240px}.ol-filter-actions[data-v-68dfbebc]{display:flex;flex:0 0 auto;flex-wrap:nowrap;gap:8px}.ol-filter-total[data-v-68dfbebc]{align-items:flex-end;align-self:center;display:flex;flex:0 0 auto;flex-direction:column;gap:2px;justify-content:center;line-height:1.2;margin-left:auto;min-width:250px}.ol-total-line[data-v-68dfbebc]{align-items:baseline;display:flex;gap:8px;white-space:nowrap}.ol-total-label[data-v-68dfbebc]{color:#4b5563;font-size:12px}.ol-total-value[data-v-68dfbebc]{font-size:13px}.ol-table[data-v-68dfbebc] .q-table thead th{font-size:11px;padding:4px 6px;white-space:nowrap}.ol-table[data-v-68dfbebc] .q-table tbody td{font-size:11px;padding:3px 6px}.ol-col-multiline[data-v-68dfbebc]{display:block;display:-webkit-box;line-height:1.15;overflow:hidden;text-overflow:ellipsis;white-space:normal!important;word-break:break-word;-webkit-box-orient:vertical;-webkit-line-clamp:2;max-height:2.35em}.ol-col-cari[data-v-68dfbebc]{max-width:160px}.ol-col-short[data-v-68dfbebc]{max-width:88px}.ol-col-desc[data-v-68dfbebc]{max-width:160px}.ol-pack-rate-cell[data-v-68dfbebc]{font-weight:700}.pack-rate-danger[data-v-68dfbebc]{color:#c62828}.pack-rate-warn[data-v-68dfbebc]{color:#8a6d00}.pack-rate-ok[data-v-68dfbebc]{color:#1f7a4f}@media (max-width:1440px){.ol-filter-row[data-v-68dfbebc]{align-items:flex-start;flex-wrap:wrap}.ol-filter-actions[data-v-68dfbebc]{flex-wrap:wrap}.ol-filter-input[data-v-68dfbebc]{flex:1 1 140px}.ol-filter-total[data-v-68dfbebc]{align-items:flex-start;margin-left:0;margin-top:6px;min-width:100%}.ol-total-line[data-v-68dfbebc]{justify-content:space-between;width:100%}}

View File

@@ -1 +0,0 @@
.bulk-close-page[data-v-734820af]{padding:10px}.bulk-filter-bar[data-v-734820af]{margin-bottom:8px}.bulk-filter-row[data-v-734820af]{align-items:flex-start;display:flex;flex-wrap:wrap;gap:10px}.bulk-search[data-v-734820af]{flex:1 1 420px;max-width:520px;min-width:320px}.bulk-filter-actions[data-v-734820af]{display:flex;flex-wrap:wrap;gap:8px}.bulk-summary[data-v-734820af]{align-items:flex-end;display:flex;flex-direction:column;font-size:12px;margin-left:auto;min-width:140px}.bulk-table[data-v-734820af] .q-table thead th{font-size:11px;font-weight:700;white-space:nowrap}.bulk-table[data-v-734820af] .q-table tbody td{font-size:11px;white-space:nowrap}.pack-rate-danger[data-v-734820af]{color:#c62828}.pack-rate-warn[data-v-734820af]{color:#8a6d00}.pack-rate-ok[data-v-734820af]{color:#1f7a4f}@media (max-width:1200px){.bulk-summary[data-v-734820af]{align-items:flex-start;margin-left:0}}

View File

@@ -1 +0,0 @@
.rdp-list-page[data-v-a17e51d4]{--rdp-header-h:56px;--rdp-filter-h:96px;background:#fff;display:flex;flex-direction:column;height:calc(100vh - var(--rdp-header-h));overflow:auto;padding:10px}.rdp-filter-bar[data-v-a17e51d4]{align-items:center;background:#fff;border-bottom:1px solid #ddd;box-shadow:0 1px 2px #0000000f;display:flex;margin-bottom:8px;min-height:var(--rdp-filter-h);padding:10px 12px;position:sticky;top:0;z-index:600}.rdp-filter-row[data-v-a17e51d4]{align-items:flex-end;display:flex;flex-wrap:nowrap;gap:12px}.rdp-filter-input[data-v-a17e51d4]{max-width:300px;min-width:180px}.rdp-search[data-v-a17e51d4]{flex:1 1 360px;max-width:520px;min-width:300px}.rdp-config-menus[data-v-a17e51d4],.rdp-filter-actions[data-v-a17e51d4]{align-items:center;display:flex;flex:0 0 auto;gap:8px;white-space:nowrap}.rdp-menu-list[data-v-a17e51d4]{max-height:420px;min-width:260px}.rdp-summary[data-v-a17e51d4]{align-self:center;background:#f9fafb;border:1px solid #e0e0e0;border-radius:6px;color:#4b5563;font-size:12px;margin-left:auto;padding:8px 12px;white-space:nowrap}.rdp-table[data-v-a17e51d4] .q-table__middle{max-height:none!important;overflow:visible!important}.rdp-table[data-v-a17e51d4] .q-table thead th{background:#fff;box-shadow:0 2px 4px #00000014;font-size:11px;position:sticky;top:var(--rdp-filter-h);white-space:nowrap;z-index:500}.rdp-table[data-v-a17e51d4] .q-table tbody td{font-size:11px;padding:3px 6px}.rdp-table[data-v-a17e51d4] .q-checkbox__inner{pointer-events:none}.rdp-table[data-v-a17e51d4] .freeze-col{background:#fff;position:sticky;z-index:510}.rdp-table[data-v-a17e51d4] thead .freeze-col{background:#fff;z-index:520}.rdp-table[data-v-a17e51d4] .freeze-1{left:0}.rdp-table[data-v-a17e51d4] .freeze-2{left:56px}.rdp-table[data-v-a17e51d4] .freeze-3{left:276px}@media (max-width:1400px){.rdp-filter-row[data-v-a17e51d4]{align-items:flex-start;flex-wrap:wrap}.rdp-config-menus[data-v-a17e51d4],.rdp-filter-actions[data-v-a17e51d4]{flex-wrap:wrap}.rdp-summary[data-v-a17e51d4]{margin-left:0;margin-top:6px;min-width:100%}}

View File

@@ -1 +0,0 @@
.perm-gateway[data-v-57a9abef]{padding:24px}

View File

@@ -1 +0,0 @@
.user-gateway-page[data-v-7b115e06]{background:#fafafa}.gateway-container[data-v-7b115e06]{max-width:900px;padding:24px;width:100%}.gateway-header[data-v-7b115e06]{text-align:center}.gateway-actions[data-v-7b115e06]{justify-content:center}.gateway-card[data-v-7b115e06]{transition:all .2s ease;width:280px}.gateway-card[data-v-7b115e06]:hover{box-shadow:0 8px 24px #00000014;transform:translateY(-4px)}

View File

@@ -1 +0,0 @@
.with-bg{min-height:100%;position:relative}.with-bg:before{background:url(/images/Baggi-tekstilas-logolu.jpg) no-repeat top;background-size:400px auto;content:"";inset:0;opacity:.15;pointer-events:none;position:absolute;z-index:0}.with-bg>*{position:relative;z-index:1}.q-page{margin-top:5px}@media (max-width:768px){.with-bg:before{background-size:260px auto}}.filter-sticky{position:sticky;top:56px;z-index:300}.filter-collapsible,.filter-sticky{background:#fff}.table-scroll{height:calc(100vh - 56px);margin-top:0;overflow-x:auto;overflow-y:auto;position:relative}.sticky-table .q-table__middle{max-height:none!important;overflow:visible!important}.sticky-table .q-table__top{background:#fff;box-shadow:0 2px 4px #00000014;position:sticky;top:0;z-index:220}.sticky-table thead th{background:#fff;position:sticky;top:40px;z-index:210}.sticky-bar{background:#fff;border-bottom:1px solid #ddd;padding:4px 8px;position:sticky;top:0;z-index:230}.sticky-table thead th{max-width:400px;min-width:80px;overflow:auto;resize:horizontal}.sticky-table td{font-size:.95rem;font-weight:600;line-height:1.2rem;max-width:400px;min-width:80px;overflow-wrap:break-word!important;padding:4px 8px!important;white-space:normal!important;word-break:break-word!important}.baggi-ppct{display:block;margin:30px auto 0;max-width:400px;opacity:.4}.col-desc{font-size:.75rem!important;line-height:1.1rem;max-width:220px!important;min-width:180px!important;overflow-wrap:break-word;white-space:normal!important;width:220px!important;word-break:break-word!important}.custom-table{font-size:.8rem}.custom-table th{background:#fff;color:#222;font-weight:800}.custom-table td{color:#333;font-weight:600}.custom-subtable{background:#fafafa;font-size:.72rem}.custom-subtable th{background:#f9f9f9;color:#555;font-weight:500}.custom-subtable td{color:#666;font-weight:400}.col-narrow{font-size:.72rem;max-width:90px;overflow:hidden;padding:2px 6px!important;text-overflow:ellipsis;white-space:nowrap}.group-row{background:#f1f1f1!important;border-bottom:2px solid #ccc;border-top:2px solid #ccc;color:#222;font-weight:700!important}.balance-card{align-items:center;border-radius:8px;display:flex;justify-content:center;min-height:120px;width:100%}.q-table td[data-col=BELGE_NO],.q-table td[data-col=Belge_No],.q-table td[data-col=belge_no]{color:var(--q-primary)!important;font-weight:600!important}.permissions-toolbar{align-items:center;background:#fff;border-bottom:1px solid #ddd;display:flex;gap:12px;padding:8px 16px;position:sticky;top:42px;z-index:300}.permissions-table-scroll{height:calc(100vh - 112px);overflow-x:auto;overflow-y:auto;position:relative}.permissions-table .q-table__middle{max-height:none!important;overflow:auto!important;padding-top:0}.permissions-table thead th{background:#fff;box-shadow:0 2px 4px #00000014;top:10px;z-index:210}.permissions-table td{background:#fff;font-size:.95rem;line-height:1.2rem;max-width:400px;min-width:80px;overflow-wrap:break-word!important;padding:4px 8px!important;white-space:normal!important;word-break:break-word!important}.permissions-table .permissions-sticky-col{background:#fff;box-shadow:2px 0 4px #0000000a;left:0;position:sticky;z-index:205}:root{--header-h:0px;--filter-h:72px;--save-h:60px;--grid-header-h:172px;--sub-header-h:34px;--drawer-w:240px;--col-model:90px;--col-renk:80px;--col-ana:100px;--col-alt:100px;--col-aciklama:140px;--col-adet:70px;--col-fiyat:70px;--col-pb:70px;--col-tutar:70px;--grp-title-w:90px;--grp-title-gap:4px;--beden-w:44px;--beden-h:28px;--beden-count:16;--baggi-gold:#c9a227;--baggi-gold-pale:#fff9e6;--baggi-gold-light:#fff7d2;--baggi-cream:#fffef9;--baggi-gray-border:#bbb}*,:after,:before{box-sizing:border-box}body,html{height:100%;margin:0}body{background:#fff;color:#222;font-family:Inter,Segoe UI,Arial,sans-serif;font-size:14px;line-height:1.4}#q-app,.q-page-container{margin:0;padding:0}.q-layout__page{top:0!important}.order-page{background:#fff;display:flex;flex-direction:column;height:calc(100vh - var(--header-h));overflow-x:visible;overflow-y:auto}.body--drawer-left-open .q-page-container{margin-left:var(--drawer-w);width:calc(100% - var(--drawer-w))}.body--drawer-left-closed .q-page-container{margin-left:0;width:100%}.order-scroll-x{flex:1}.order-page::-webkit-scrollbar-thumb,.order-scroll-x::-webkit-scrollbar{height:8px;width:8px}.order-scroll-x::-webkit-scrollbar-thumb{background:#c0a75e;border-radius:4px}.order-scroll-x::-webkit-scrollbar-track{background:#f9f5e6}.q-header{box-shadow:0 1px 2px #00000014;position:sticky;top:0;z-index:1000}.sticky-stack{background:#fff;box-shadow:0 1px 3px #0000000d;display:flex;flex-direction:column;margin-top:0!important;position:sticky;top:var(--header-h);z-index:950}.filter-bar{background:#fafafa;margin-top:0!important;padding:12px 24px}.filter-bar,.save-toolbar{border-bottom:1px solid #ddd}.save-toolbar{align-items:center;background:var(--baggi-gold-pale);border-top:1px solid #ddd;display:flex;justify-content:space-between;padding:10px 16px;z-index:940}.save-toolbar .label{color:#6a5314;font-weight:700}.save-toolbar .value{color:#000;font-weight:700}.save-toolbar .q-btn{border-radius:6px;font-weight:600;text-transform:none}.order-grid-header{background:var(--baggi-cream);border-bottom:2px solid var(--baggi-gray-border);box-shadow:0 2px 3px #0000000d;display:grid;grid-auto-flow:column;grid-template-columns:var(--col-model) var(--col-renk) var(--col-ana) var(--col-alt) var(--col-aciklama) calc(var(--grp-title-w) + var(--grp-title-gap) + var(--beden-w)*var(--beden-count)) var(--col-adet) var(--col-fiyat) var(--col-pb) var(--col-tutar) var(--col-termin);position:sticky;top:calc(var(--header-h) + var(--filter-h) + var(--save-h));z-index:700}.order-grid-header .col-fixed{align-items:center;background:var(--baggi-gold-light);border:1px solid #aaa;display:flex;font-size:12.5px;font-weight:700;height:var(--grid-header-h);justify-content:center;transform:rotate(180deg);writing-mode:vertical-lr}.order-grid-header .aciklama-col{background:#fff9c4;border-right:2px solid #a6a6a6}.order-grid-header .beden-block{background:#fff;border:1px solid #ccc;display:flex;flex-direction:column;height:var(--grid-header-h)}.order-grid-header .grp-row{align-items:center;display:flex;height:var(--beden-h)}.order-grid-header .grp-title{font-size:12px;font-weight:700;padding-right:4px;text-align:right;width:var(--grp-title-w)}.order-grid-header .grp-body{display:grid;grid-auto-columns:var(--beden-w);grid-auto-flow:column}.order-grid-header .grp-cell.hdr{align-items:center;border:1px solid #bbb;display:flex;font-size:11.5px;font-weight:600;height:var(--beden-h);justify-content:center;width:var(--beden-w)}.order-grid-header .total-row{align-items:stretch;background:#fff59d;display:flex;justify-content:space-between}.order-grid-header .total-cell{align-items:center;background:var(--baggi-gold-pale);border-right:1px solid #bbb;display:flex;font-size:12px;font-weight:700;justify-content:center;transform:rotate(180deg);width:var(--col-adet);writing-mode:vertical-lr}.order-sub-header{align-items:center;background:linear-gradient(90deg,#fffbe9,#fff4c4 50%,#fff1b0);border-bottom:1px solid #d6c06a;border-top:1px solid #d6c06a;box-sizing:border-box;display:grid;grid-auto-flow:column;grid-template-columns:var(--col-model) var(--col-renk) var(--col-ana) var(--col-alt) var(--col-aciklama) calc(var(--grp-title-w) + var(--grp-title-gap) + var(--beden-w)*var(--beden-count)) var(--col-adet) var(--col-fiyat) var(--col-pb) var(--col-tutar) var(--col-termin);height:var(--sub-header-h);justify-items:stretch;margin-right:0!important;margin:0!important;min-height:var(--sub-header-h);padding-right:0!important;padding:0!important;position:sticky;top:calc(var(--header-h) + var(--filter-h) + var(--save-h) + var(--grid-header-h));z-index:650}:root{--col-termin:142px}.order-sub-header .sub-left{align-items:center;color:#2b1f05;display:flex;font-weight:800;grid-column:1/span 5;padding-left:6px}.order-sub-header .sub-center{align-items:center;box-sizing:border-box;display:grid;grid-auto-columns:var(--beden-w);grid-auto-flow:column;grid-column:6/7;height:100%;justify-content:start;margin-left:var(--grp-title-gap);padding-left:var(--grp-title-w);width:calc(var(--grp-title-w) + var(--grp-title-gap) + var(--beden-w)*var(--beden-count))}.order-sub-header .beden-cell{align-items:center;background:#fffdf3;border:1px solid #d8c16b;border-right:none;box-sizing:border-box;display:flex;font-size:12px;font-weight:600;height:100%;justify-content:center;width:var(--beden-w)}.order-sub-header .beden-cell:last-child{border-right:1px solid #d8c16b}.order-sub-header .sub-right{align-items:flex-end;color:#3b2f09;display:flex;flex-direction:column;font-size:13.5px;font-weight:900;grid-column:7/-1;justify-content:center;line-height:1.3;padding-right:0;text-align:right;text-transform:uppercase;transform:translateX(-60px)}.order-sub-header:hover{background:linear-gradient(90deg,#fff9cf,#fff3b0 70%,#ffe88f)}:root{--sub-header-h:60px}.order-sub-header{overflow:hidden}.order-grid-body{background:#fff;margin-top:0!important;padding-top:var(--sub-header-h);position:relative;z-index:100}.summary-row{display:grid;grid-template-columns:var(--col-model) var(--col-renk) var(--col-ana) var(--col-alt) var(--col-aciklama) calc(var(--grp-title-w) + var(--grp-title-gap) + var(--beden-w)*var(--beden-count)) var(--col-adet) var(--col-fiyat) var(--col-pb) var(--col-tutar) var(--col-termin)}.summary-row .cell{box-sizing:border-box;color:#222;font-size:13px;height:var(--beden-h)}.summary-row.row-closed{background:#f5f5f5;opacity:.55}.summary-row.row-closed:hover{background:#f5f5f5!important}.summary-row:nth-child(odd){background:#fffef9}.summary-row .grp-area{display:flex;flex-direction:column;justify-content:center;transform:translateX(calc(var(--grp-title-w) - var(--beden-w)))}.summary-row .grp-row{display:grid;grid-auto-columns:var(--beden-w);grid-auto-flow:column}.summary-row .grp-row .cell.beden{align-items:center;border:1px solid #ddd;display:flex;font-size:12px;height:var(--beden-h);justify-content:center;width:var(--beden-w)}.cell.beden.ghost{border:1px solid #0000!important;opacity:0;pointer-events:none}.summary-row .cell.adet,.summary-row .cell.fiyat,.summary-row .cell.pb,.summary-row .cell.termin,.summary-row .cell.tutar{border-left:none!important;color:#000;font-weight:600;height:100%}.summary-row .cell.tutar{border-right:none!important;justify-content:flex-end;padding-right:8px;text-align:right}.summary-row .cell.termin{align-items:center;background:#fffef9;justify-content:center;min-width:var(--col-termin)}.summary-row .cell.termin .q-input{box-sizing:border-box;max-width:142px!important;width:100%}.summary-row .cell.termin input{font-size:13px;text-align:center}.editor{background:#fffef9;border-top:1px solid #ddd;margin-top:24px;padding:16px;position:relative;z-index:50}.editor:before{background:linear-gradient(90deg,#c9a227,#e5d28b,#fff7d2);border-radius:2px;content:"";display:block;height:4px;margin-bottom:12px}.editor .q-input,.editor .q-select{font-size:14px;margin-bottom:8px}.cell.termin .termin-label{align-items:center;background:#fffef9;border-left:1px solid #ccc;box-sizing:border-box;color:#222;display:flex;font-size:13px;font-weight:600;height:100%;justify-content:center;width:100%}@media (max-width:1024px){:root{--beden-w:40px;--col-aciklama:120px}.order-grid-header .col-fixed{font-size:11px}.order-sub-header{font-size:12.5px}}@media (max-width:768px){:root{--beden-w:36px;--col-model:70px;--col-renk:60px;--col-aciklama:100px}.order-page{font-size:13px}.order-grid-header .total-cell{font-size:10.5px}}.summary-row .cell{align-items:center;display:flex;height:auto;justify-content:center;padding:4px 6px;text-align:center;white-space:normal;word-wrap:break-word}.summary-row .grp-area,.summary-row .grp-row,.summary-row .grp-row .cell.beden{align-items:center;height:100%}.summary-row .cell.aciklama{align-items:flex-start!important;background:#fff!important;border-right:1px solid #ccc!important;box-sizing:border-box!important;display:flex!important;flex-direction:column!important;font-size:13px!important;grid-column:5/6!important;justify-content:flex-start!important;line-height:1.4!important;margin-right:-92px!important;min-height:36px!important;overflow-wrap:break-word!important;padding:6px 12px!important;position:relative!important;text-align:left!important;white-space:normal!important;width:calc(var(--col-aciklama) + 92px)!important;word-break:break-word!important;z-index:10!important}.order-grid-header .col-fixed,.summary-row .cell,.summary-row .grp-row .cell.beden{border-color:#bbb!important}.summary-row .cell:not(:last-child){border-right:1px solid #bdbdbd!important}.summary-row{border-bottom:1px solid #ccc}.summary-row:last-child{border-bottom:2px solid #b7a33a}.summary-row .cell,.summary-row .grp-row .cell.beden{border-bottom:1px solid #ddd!important}.summary-row:hover{background:#fffce0}.summary-row.is-editing{background:#fff3cd;outline:2px solid #caa83f;z-index:2}.editor .q-btn:hover{background:#d2b04d;color:#fff}.summary-row:hover .cell,.summary-row:hover .grp-row .cell.beden{border-bottom:1px solid #ccc!important}.stok-red{color:#e53935;font-weight:600}.stok-yellow{color:#f9a825;font-weight:600}.stok-green{color:#43a047;font-weight:600}.q-banner.rounded-borders{border-radius:8px}.order-gateway{background:linear-gradient(145deg,#fff,#fafafa);height:100%}.order-btn{border-radius:12px;font-size:1.2rem;min-width:280px;padding:20px 40px;transition:all .2s ease}.order-btn:hover{box-shadow:0 4px 12px #00000026;transform:translateY(-3px)}.body--drawer-left-open .order-page{overflow-x:visible;width:calc(100vw - var(--drawer-w))}.order-scroll-x{box-sizing:border-box;max-width:100%}.order-grid-body,.order-grid-header,.order-sub-header{box-sizing:border-box;min-width:fit-content;width:100%}.body--drawer-left-open .filter-bar,.body--drawer-left-open .order-grid-body,.body--drawer-left-open .order-grid-header,.body--drawer-left-open .order-sub-header,.body--drawer-left-open .save-toolbar{box-sizing:border-box;margin-left:0;margin-right:0;overflow-x:hidden;width:calc(100vw - var(--drawer-w))}.body--drawer-left-closed .filter-bar,.body--drawer-left-closed .order-grid-body,.body--drawer-left-closed .order-grid-header,.body--drawer-left-closed .order-sub-header,.body--drawer-left-closed .save-toolbar{width:100vw}.order-grid-body,.order-grid-header,.order-sub-header{border-right:2px solid var(--baggi-gold)}.body--drawer-left-open .filter-bar,.body--drawer-left-open .order-grid-body,.body--drawer-left-open .order-grid-header,.body--drawer-left-open .order-page,.body--drawer-left-open .order-sub-header,.body--drawer-left-open .save-toolbar{margin-right:0!important;overflow-x:visible!important;padding-right:0!important;width:calc(100vw - var(--drawer-w) - 8px)}.order-grid-body{border-right:2px solid var(--baggi-gold)}.order-scroll-x{align-items:flex-start;background:#fff;display:flex;flex-direction:column;overflow-x:auto;overflow-y:visible}.filter-bar,.order-grid-header,.order-sub-header,.save-toolbar{box-sizing:border-box;min-width:100%;width:fit-content}.order-grid-body{box-sizing:border-box;width:fit-content}.summary-row.row-closed{opacity:.65;pointer-events:none}.summary-row.row-closed,.summary-row.row-closed:hover{background:#e6e6e6!important}.summary-row.row-closed.is-editing{outline:none!important}.filter-bar,.order-grid-body,.order-grid-header,.order-sub-header,.save-toolbar{border-right:none!important;margin-right:0!important;padding-right:0!important}.summary-row.row-error{background:#c1001514}.row-error-icon{left:4px;position:absolute;top:50%;transform:translateY(-50%)}.body--drawer-left-closed .order-scroll-x,.body--drawer-left-open .order-scroll-x{overflow-x:auto;width:100%}:root{--ol-header-h:56px;--ol-filter-h:96px}.ol-page{background:#fff;display:flex;flex-direction:column;height:calc(100vh - var(--ol-header-h));overflow:auto}.ol-filter-bar{align-items:center;box-shadow:0 1px 2px #0000000f;display:flex;min-height:var(--ol-filter-h);padding:10px 16px}.ol-table .q-table__middle{max-height:none!important;overflow:visible!important}.ol-table thead th{background:#fff;box-shadow:0 2px 4px #00000014;font-weight:700;position:sticky;top:var(--ol-filter-h);z-index:500}.ol-table .q-table__body .q-tr:nth-child(odd){background-color:#f7f7f7!important}.ol-table .q-table__body .q-tr:nth-child(2n){background-color:#fff!important}.ol-table .q-table__body .q-tr:hover{background-color:#fff7d1!important;transition:background-color .15s ease}.ol-table .q-td{font-size:.9rem;line-height:1.3;padding:6px 8px!important}.q-header{z-index:1000!important}.q-drawer{z-index:950!important}@media (max-width:768px){:root{--ol-filter-h:64px}.ol-filter-bar{padding:8px 12px}}.ol-table .q-table__body .q-tr:nth-child(odd),.ol-table tbody tr:nth-child(odd){background-color:#faf8ef!important}.ol-table .q-table__body .q-tr:nth-child(2n),.ol-table tbody tr:nth-child(2n){background-color:#fff!important}.ol-table .q-table__body .q-tr:hover,.ol-table tbody tr:hover{background-color:#fff4cc!important;transition:background-color .2s ease}.ol-qbanner{background:#f9fafb;border:1px solid #e0e0e0;border-radius:6px;padding:8px 12px}.ol-qbanner-amount{color:#1976d2;margin-left:6px}.ol-col-multiline{line-height:1.25rem;overflow:hidden;white-space:normal!important;word-break:break-word}.ol-col-cari{font-size:.88rem;font-weight:600;max-width:200px;min-width:150px}.ol-col-cari,.ol-col-desc{display:-webkit-box;-webkit-box-orient:vertical}.ol-col-desc{color:#444;font-size:.82rem;max-width:220px;min-width:160px}.ol-table .q-td.ol-col-cari,.ol-table .q-td.ol-col-desc{padding-bottom:6px!important;padding-top:6px!important}.ol-table th.ol-col-cari,.ol-table th.ol-col-desc{white-space:nowrap}.ol-filter-bar{background:#fff;border-bottom:1px solid #ddd;padding:10px 12px;position:sticky;top:0;z-index:600}.ol-filter-row{align-items:flex-end;display:flex;flex-wrap:nowrap;gap:12px}.ol-filter-input{max-width:260px;min-width:180px}.ol-search{min-width:280px}.ol-filter-actions{align-items:center;display:flex;gap:8px;white-space:nowrap}.ol-filter-total{background:#f9fafb;border:1px solid #e0e0e0;border-radius:6px;margin-left:auto;padding:8px 12px;white-space:nowrap}@media (max-width:1200px){.ol-filter-row{flex-wrap:wrap;row-gap:8px}.ol-filter-total{justify-content:flex-end;width:100%}}.order-gateway{min-height:100vh}.draft-card{max-width:90vw;width:320px}.act-page{background:#fff;height:calc(100vh - 56px);overflow:auto}.act-filter-bar{background:#fff;border-bottom:1px solid #ddd;box-shadow:0 1px 2px #0000000f;padding:10px 16px;position:sticky;top:0;z-index:620}.act-filter-row{align-items:flex-end;display:flex;flex-wrap:nowrap;gap:12px}.act-filter-input{max-width:240px;min-width:160px}.act-filter-wide{min-width:260px}.act-filter-actions{display:flex;gap:8px;margin-left:auto;white-space:nowrap}.act-table{font-size:.85rem}.act-table thead th{background:#fff;box-shadow:0 2px 4px #00000014;font-weight:700;position:sticky;top:56px;z-index:500}.act-table tbody tr:nth-child(odd){background:#faf8ef}.act-table tbody tr:nth-child(2n){background:#fff}.act-table tbody tr:hover{background:#fff4cc}.act-table .q-td{font-weight:600;line-height:1.25;padding:6px 8px!important}.act-row-success{background:#43a0470f}.act-row-fail{background:#d32f2f0f}.act-badge-ok{background:#43a047}.act-badge-fail{background:#e53935}.act-col-narrow{max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.act-col-route{font-size:.8rem;max-width:260px;white-space:normal;word-break:break-word}.act-col-meta{color:#555;font-size:.75rem}.ol-col-piyasa{padding-bottom:6px!important;padding-top:6px!important;vertical-align:top}.piyasa-wrap{align-content:flex-start;column-gap:6px;display:flex;flex-wrap:wrap;max-height:none;overflow:visible;row-gap:4px}.piyasa-chip{flex:0 0 calc(25% - 6px);font-size:11px;font-weight:600;line-height:1.1;max-width:calc(25% - 6px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.user-detail-page{background:#fafafa}.image-preview{border-radius:6px;width:100%}.image-thumb{border-radius:4px;width:100%}.workorder-page{padding-bottom:80px}.permissions-page{background:#fff;display:flex;flex-direction:column;height:calc(100vh - 56px)}.permissions-table-scroll{background:#fff;flex:1;overflow:auto}.permissions-table{font-size:.85rem}.permissions-table thead th{background:var(--baggi-cream);box-shadow:0 2px 3px #0000000f;color:#222;font-weight:800;position:sticky;top:0;z-index:300}.permissions-table td{color:#333;font-weight:600;padding:6px 8px!important}.permissions-table tbody tr:nth-child(odd){background:#fffef7}.permissions-table tbody tr:nth-child(2n){background:#fff}.permissions-table tbody tr:hover{background:#fff4cc}.permissions-sticky-col{background:#fff;box-shadow:2px 0 4px #0000000a;font-weight:700;left:0;position:sticky;z-index:250}.permissions-table .q-th .column{gap:2px}

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html><head><title>Baggi SS</title><meta charset=utf-8><meta name=description content="A Quasar Project"><meta name=format-detection content="telephone=no"><meta name=msapplication-tap-highlight content=no><meta name=viewport content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width"><link rel=icon type=image/png sizes=128x128 href=/icons/favicon-128x128.png><link rel=icon type=image/png sizes=96x96 href=/icons/favicon-96x96.png><link rel=icon type=image/png sizes=32x32 href=/icons/favicon-32x32.png><link rel=icon type=image/png sizes=16x16 href=/icons/favicon-16x16.png><link rel=icon type=image/ico href=/favicon.ico><script defer src=/js/vendor.9ea1812a.js></script><script defer src=/js/app.d0936c73.js></script><link href=/css/vendor.724dcfab.css rel=stylesheet><link href=/css/app.53116624.css rel=stylesheet></head><body><div id=q-app></div></body></html>

9
ui/package-lock.json generated
View File

@@ -19,7 +19,8 @@
}, },
"devDependencies": { "devDependencies": {
"@quasar/app-webpack": "^4.1.0", "@quasar/app-webpack": "^4.1.0",
"autoprefixer": "^10.4.2" "autoprefixer": "^10.4.2",
"baseline-browser-mapping": "^2.9.19"
}, },
"engines": { "engines": {
"node": "^28 || ^26 || ^24 || ^22 || ^20 || ^18", "node": "^28 || ^26 || ^24 || ^22 || ^20 || ^18",
@@ -3855,9 +3856,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.8.12", "version": "2.9.19",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.12.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
"integrity": "sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==", "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {

View File

@@ -23,7 +23,8 @@
}, },
"devDependencies": { "devDependencies": {
"@quasar/app-webpack": "^4.1.0", "@quasar/app-webpack": "^4.1.0",
"autoprefixer": "^10.4.2" "autoprefixer": "^10.4.2",
"baseline-browser-mapping": "^2.9.19"
}, },
"browserslist": [ "browserslist": [
"last 10 Chrome versions", "last 10 Chrome versions",

View File

@@ -2,6 +2,8 @@
import { defineConfig } from '#q-app/wrappers' import { defineConfig } from '#q-app/wrappers'
export default defineConfig(() => { export default defineConfig(() => {
const apiBaseUrl = (process.env.VITE_API_BASE_URL || '/api').trim()
return { return {
/* ===================================================== /* =====================================================
@@ -33,6 +35,9 @@ export default defineConfig(() => {
===================================================== */ ===================================================== */
build: { build: {
vueRouterMode: 'hash', vueRouterMode: 'hash',
env: {
VITE_API_BASE_URL: apiBaseUrl
},
esbuildTarget: { esbuildTarget: {
browser: ['es2022', 'firefox115', 'chrome115', 'safari14'], browser: ['es2022', 'firefox115', 'chrome115', 'safari14'],
@@ -52,14 +57,15 @@ export default defineConfig(() => {
port: 9000, port: 9000,
open: true, open: true,
// DEV proxy (CORSsuz) // DEV proxy (CORS'suz)
proxy: { proxy: [
'/api': { {
context: ['/api'],
target: 'http://localhost:8080', target: 'http://localhost:8080',
changeOrigin: true, changeOrigin: true,
secure: false secure: false
} }
} ]
}, },
/* ===================================================== /* =====================================================
@@ -119,3 +125,4 @@ export default defineConfig(() => {
} }
} }
}) })

View File

@@ -1,83 +0,0 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* 1. DO NOT edit this file directly as it won't do anything.
* 2. EDIT the original quasar.config file INSTEAD.
* 3. DO NOT git commit this file. It should be ignored.
*
* This file is still here because there was an error in
* the original quasar.config file and this allows you to
* investigate the Node.js stack error.
*
* After you fix the original file, this file will be
* deleted automatically.
**/
// quasar.config.js
import { defineConfig } from "@quasar/app-webpack/wrappers";
var quasar_config_default = defineConfig(() => {
return {
// ✅ UYGULAMA KİMLİĞİ (WEB'DE GÖRÜNEN İSİM)
productName: "Baggi BSS",
productDescription: "Baggi Tekstil Business Support System",
// 🔹 Boot dosyaları
boot: ["axios", "dayjs"],
// 🔹 Global CSS
css: ["app.css"],
// 🔹 Ekstra icon/font setleri
extras: [
"roboto-font",
"material-icons"
],
// 🔹 Derleme Ayarları
build: {
vueRouterMode: "hash",
env: {
VITE_API_BASE_URL: "/api"
},
esbuildTarget: {
browser: ["es2022", "firefox115", "chrome115", "safari14"],
node: "node20"
}
},
// 🔹 Geliştirme Sunucusu
devServer: {
server: { type: "http" },
port: 9e3,
open: true
},
// 🔹 Quasar Framework ayarları
framework: {
config: {
notify: { position: "top", timeout: 2500 }
},
lang: "tr",
plugins: ["Loading", "Dialog", "Notify"]
},
animations: [],
ssr: {
prodPort: 3e3,
middlewares: ["render"],
pwa: false
},
pwa: {
workboxMode: "GenerateSW"
},
capacitor: {
hideSplashscreen: true
},
electron: {
preloadScripts: ["electron-preload"],
inspectPort: 5858,
bundler: "packager",
builder: { appId: "baggisowtfaresystem" }
},
bex: {
extraScripts: []
}
};
});
export {
quasar_config_default as default
};

View File

@@ -0,0 +1,125 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* 1. DO NOT edit this file directly as it won't do anything.
* 2. EDIT the original quasar.config file INSTEAD.
* 3. DO NOT git commit this file. It should be ignored.
*
* This file is still here because there was an error in
* the original quasar.config file and this allows you to
* investigate the Node.js stack error.
*
* After you fix the original file, this file will be
* deleted automatically.
**/
// quasar.config.js
import { defineConfig } from "@quasar/app-webpack/wrappers";
var quasar_config_default = defineConfig(() => {
const apiBaseUrl = (process.env.VITE_API_BASE_URL || "/api").trim();
return {
/* =====================================================
APP INFO
===================================================== */
productName: "Baggi BSS",
productDescription: "Baggi Tekstil Business Support System",
/* =====================================================
BOOT FILES
===================================================== */
boot: ["dayjs"],
/* =====================================================
GLOBAL CSS
===================================================== */
css: ["app.css"],
/* =====================================================
ICONS / FONTS
===================================================== */
extras: [
"roboto-font",
"material-icons"
],
/* =====================================================
BUILD (PRODUCTION)
===================================================== */
build: {
vueRouterMode: "hash",
env: {
VITE_API_BASE_URL: apiBaseUrl
},
esbuildTarget: {
browser: ["es2022", "firefox115", "chrome115", "safari14"],
node: "node20"
},
// Cache & performance
gzip: true,
preloadChunks: true
},
/* =====================================================
DEV SERVER (LOCAL)
===================================================== */
devServer: {
server: { type: "http" },
port: 9e3,
open: true,
// DEV proxy (CORS'suz)
proxy: [
{
context: ["/api"],
target: "http://localhost:8080",
changeOrigin: true,
secure: false
}
]
},
/* =====================================================
QUASAR FRAMEWORK
===================================================== */
framework: {
config: {
notify: {
position: "top",
timeout: 2500
}
},
lang: "tr",
plugins: [
"Loading",
"Dialog",
"Notify"
]
},
animations: [],
/* =====================================================
SSR / PWA (DISABLED)
===================================================== */
ssr: {
prodPort: 3e3,
middlewares: ["render"],
pwa: false
},
pwa: {
workboxMode: "GenerateSW"
},
/* =====================================================
MOBILE / DESKTOP
===================================================== */
capacitor: {
hideSplashscreen: true
},
electron: {
preloadScripts: ["electron-preload"],
inspectPort: 5858,
bundler: "packager",
builder: {
appId: "baggisowtfaresystem"
}
},
bex: {
extraScripts: []
}
};
});
export {
quasar_config_default as default
};

View File

@@ -21,7 +21,7 @@
} }
.q-page { .q-page {
margin-top: 5px; margin-top: 0;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@@ -288,6 +288,11 @@ body {
overflow-y: auto; overflow-y: auto;
overflow-x: visible; overflow-x: visible;
background: #fff; background: #fff;
padding-top: 0 !important;
}
/* Quasar header offsetunu sadece order sayfasında sıfırla */
.q-page-container .order-page {
margin-top: calc(-1 * var(--header-h) - 58px);
} }
.body--drawer-left-open .q-page-container { .body--drawer-left-open .q-page-container {
@@ -344,16 +349,33 @@ body {
.filter-bar { .filter-bar {
background: #fafafa; background: #fafafa;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
padding: 12px 24px; padding: 8px 12px;
margin-top:0 !important; margin-top:0 !important;
} }
.filter-bar .q-field__label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.1;
font-size: 12px;
}
.filter-bar .q-field__control,
.filter-bar .q-field__native,
.filter-bar .q-field__marginal {
min-height: 36px;
}
.filter-bar-desc {
padding: 0 12px 6px;
background: #fafafa;
border-bottom: 1px solid #ddd;
}
/* 🔹 Save toolbar */ /* 🔹 Save toolbar */
.save-toolbar { .save-toolbar {
background: var(--baggi-gold-pale); background: var(--baggi-gold-pale);
border-top: 1px solid #ddd; border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
padding: 10px 16px; padding: 6px 10px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@@ -454,8 +476,9 @@ body {
.order-grid-header .total-row { .order-grid-header .total-row {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
justify-content: space-between; justify-content: flex-start;
background: #fff59d; background: #fff59d;
grid-column: 7 / -1;
} }
.order-grid-header .total-cell { .order-grid-header .total-cell {
width: var(--col-adet); width: var(--col-adet);
@@ -469,6 +492,9 @@ body {
font-weight: 700; font-weight: 700;
font-size: 12px; font-size: 12px;
} }
.order-grid-header .total-cell:last-child {
width: var(--col-termin);
}
/* =========================================================== /* ===========================================================
6⃣ SUB-HEADER (ÜRÜN GRUBU BAR) — TAM HİZALANMIŞ 6⃣ SUB-HEADER (ÜRÜN GRUBU BAR) — TAM HİZALANMIŞ
=========================================================== */ =========================================================== */
@@ -614,6 +640,108 @@ body {
padding-top: var(--sub-header-h); padding-top: var(--sub-header-h);
z-index: 100; z-index: 100;
} }
.order-scroll-y.compact-grid-header {
--grid-header-h: var(--beden-h);
}
.order-scroll-y.compact-grid-header .order-grid-header .col-fixed {
writing-mode: horizontal-tb;
transform: none;
height: var(--grid-header-h);
font-size: 10px;
line-height: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 4px;
}
.order-scroll-y.compact-grid-header .order-grid-header .total-cell {
writing-mode: horizontal-tb;
transform: none;
height: var(--grid-header-h);
font-size: 10px;
line-height: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 4px;
}
.order-grid-header.compact {
height: var(--beden-h);
}
.order-grid-header.compact .beden-block {
height: var(--beden-h);
}
.order-grid-header.compact .col-fixed,
.order-grid-header.compact .total-cell {
writing-mode: horizontal-tb;
transform: none;
height: var(--beden-h);
font-size: 10px;
line-height: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 4px;
}
.order-grid-header.compact .total-cell {
width: var(--col-adet);
}
.order-grid-header.compact .total-cell:nth-child(2) {
width: var(--col-fiyat);
}
.order-grid-header.compact .total-cell:nth-child(3) {
width: var(--col-pb);
}
.order-grid-header.compact .total-cell:nth-child(4) {
width: var(--col-tutar);
}
.order-grid-header.compact .total-cell:nth-child(5) {
width: var(--col-termin);
}
.order-grid-header.compact .grp-row {
height: var(--beden-h);
}
.order-grid-header.compact .grp-row:not(:first-child) {
display: none;
}
.order-grid-header.compact .grp-title {
display: none;
}
.order-grid-header.compact .grp-row:first-child .grp-title {
display: block;
font-size: 10px;
line-height: 1;
white-space: nowrap;
padding-right: 4px;
}
.order-grid-header.compact .grp-body {
height: var(--beden-h);
align-items: center;
}
.order-grid-header.compact .grp-cell.hdr {
height: var(--beden-h);
font-size: 10px;
}
.order-scroll-y.compact-grid-header .order-grid-header .grp-title {
display: none;
}
.order-scroll-y.compact-grid-header .order-grid-header .grp-row {
align-items: center;
height: var(--beden-h);
}
.order-scroll-y.compact-grid-header .order-grid-header .grp-body {
grid-template-rows: var(--beden-h);
align-items: center;
}
.order-scroll-y.compact-grid-header .order-grid-header .grp-cell.hdr {
height: var(--beden-h);
}
.order-scroll-y.compact-grid-header .order-grid-header .total-row {
align-items: center;
}
.order-scroll-y.compact-grid-header .order-grid-body {
padding-top: var(--sub-header-h);
}
.summary-row { .summary-row {
display: grid; display: grid;
@@ -1086,7 +1214,7 @@ body {
z-index: 600; z-index: 600;
background: #fff; background: #fff;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
padding: 10px 16px; padding: 6px 10px;
box-shadow: 0 1px 2px rgba(0,0,0,0.06); box-shadow: 0 1px 2px rgba(0,0,0,0.06);
min-height: var(--ol-filter-h); min-height: var(--ol-filter-h);
display: flex; display: flex;
@@ -1309,7 +1437,7 @@ body {
z-index: 620; z-index: 620;
background: #fff; background: #fff;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
padding: 10px 16px; padding: 6px 10px;
box-shadow: 0 1px 2px rgba(0,0,0,0.06); box-shadow: 0 1px 2px rgba(0,0,0,0.06);
} }
@@ -1530,3 +1658,21 @@ body {
gap: 2px; gap: 2px;
} }
/* ===========================================================
ORDER ENTRY POPUP EDITOR
=========================================================== */
.order-editor-card {
width: 98vw;
max-width: 1900px;
}
.order-editor-card .editor {
max-height: 76vh;
overflow: auto;
}
.order-editor-dialog .q-dialog__inner--minimized {
max-width: 98vw;
}
.order-editor-dialog .q-dialog__inner > div {
width: 98vw;
max-width: 1900px;
}

View File

@@ -209,6 +209,11 @@ const menuItems = [
to: '/app/order-gateway', to: '/app/order-gateway',
permission: 'order:view' permission: 'order:view'
}, },
{
label: 'Üretime Verilen Siparişleri Güncelle',
to: '/app/orderproductionupdate',
permission: 'order:update'
},
{ {
label: 'Tamamlanan Siparişleri Toplu Kapatma', label: 'Tamamlanan Siparişleri Toplu Kapatma',
to: '/app/order-bulk-close', to: '/app/order-bulk-close',
@@ -226,25 +231,25 @@ const menuItems = [
{ {
label: 'Rol + Departman Yetkileri', label: 'Rol + Departman Yetkileri',
to: '/app/role-dept-permissions', to: '/app/role-dept-permissions',
permission: 'user:update' permission: 'system:update'
}, },
{ {
label: 'Kullanıcı Yetkileri', label: 'Kullanıcı Yetkileri',
to: '/app/user-permissions', to: '/app/user-permissions',
permission: 'user:update' permission: 'system:update'
}, },
{ {
label: 'Loglar', label: 'Loglar',
to: '/app/activity-logs', to: '/app/activity-logs',
permission: 'user:view' permission: 'system:read'
}, },
{ {
label: 'Test Mail', label: 'Test Mail',
to: '/app/test-mail', to: '/app/test-mail',
permission: 'user:insert' permission: 'system:update'
} }
] ]
@@ -258,7 +263,7 @@ const menuItems = [
{ {
label: 'Kullanıcılar', label: 'Kullanıcılar',
to: '/app/users', to: '/app/users',
permission: 'user:view' permission: 'system:read'
} }
] ]
} }

View File

@@ -1,4 +1,4 @@
<template> <template>
<!-- =========================================================== <!-- ===========================================================
🧾 ORDER ENTRY PAGE (BSSApp) 🧾 ORDER ENTRY PAGE (BSSApp)
v23 Sticky-stack + Drawer uyumlu yapı v23 Sticky-stack + Drawer uyumlu yapı
@@ -8,10 +8,11 @@
class="order-page" class="order-page"
> >
<!-- 🔄 SAYFA LOADERI --> <!-- 🔄 SAYFA LOADERI -->
<q-inner-loading :showing="loadingHeader || loadingCari || loadingModels" color="primary"> <q-inner-loading :showing="isPageBlocking" color="primary">
<q-spinner size="50px" /> <q-spinner size="50px" />
</q-inner-loading> </q-inner-loading>
<template v-if="!isPageBlocking">
<!-- ======================================================= <!-- =======================================================
🔹 STICKY STACK (Filter + Save + Header) 🔹 STICKY STACK (Filter + Save + Header)
======================================================== --> ======================================================== -->
@@ -168,8 +169,22 @@
</template> </template>
</div> </div>
</div>
<!-- 📝 Sipariş Genel Açıklaması (filter bar altında) -->
<div class="filter-bar-desc q-mt-sm">
<q-input
v-model="form.Description"
type="textarea"
label="Sipariş Genel ıklaması"
filled
dense
autogrow
maxlength="1500"
counter
placeholder="Siparişe genel ıklama giriniz (örn. teslimat, üretim notu, müşteri isteği...)"
:disable="isClosedRow"
:readonly="isViewOnly"
/>
</div> </div>
<!-- 🔹 Cari Bilgi Barı --> <!-- 🔹 Cari Bilgi Barı -->
<q-slide-transition> <q-slide-transition>
@@ -212,6 +227,14 @@
<div class="save-toolbar"> <div class="save-toolbar">
<div class="text-subtitle2 text-weight-bold">Sipariş Formu</div> <div class="text-subtitle2 text-weight-bold">Sipariş Formu</div>
<div> <div>
<q-btn
flat
color="grey-7"
class="q-ml-sm"
:label="compactGridHeader ? 'BAŞLIK GENİŞLET' : 'BAŞLIK DARALT'"
:icon="compactGridHeader ? 'unfold_more' : 'unfold_less'"
@click="compactGridHeader = !compactGridHeader"
/>
<q-btn <q-btn
v-if="isViewOnly && canExportOrder" v-if="isViewOnly && canExportOrder"
label="🖨 SİPARİŞİ YAZDIR" label="🖨 SİPARİŞİ YAZDIR"
@@ -222,7 +245,16 @@
/> />
<q-btn <q-btn
v-else-if="canSubmitOrder" v-if="canMutateRows"
label="SATIR EKLE"
color="secondary"
icon="add"
class="q-ml-sm"
@click="openNewRowEditor"
:disable="isClosedRow || isViewOnly || !canMutateRows"
/>
<q-btn
v-if="canSubmitOrder"
:label="isEditMode ? 'TÜMÜNÜ GÜNCELLE' : 'TÜMÜNÜ KAYDET'" :label="isEditMode ? 'TÜMÜNÜ GÜNCELLE' : 'TÜMÜNÜ KAYDET'"
color="primary" color="primary"
icon="save" icon="save"
@@ -231,24 +263,15 @@
:disable="!canSubmitOrder" :disable="!canSubmitOrder"
@click="confirmAndSubmit" @click="confirmAndSubmit"
/> />
<q-btn
label="YENİ SİPARİŞ"
v-if="canWriteOrder"
color="secondary"
icon="add_circle"
class="q-ml-sm"
@click="onResetEditorClick"
:disable="isClosedRow || !canWriteOrder"
/>
</div> </div>
</div> </div>
<!-- 🔹 Grid Header --> <!-- 🔹 Grid Header -->
<div class="order-grid-header"> <div class="order-grid-header" :class="{ compact: compactGridHeader }">
<div class="col-fixed model">MODEL</div> <div class="col-fixed model">MODEL</div>
<div class="col-fixed renk">RENK</div> <div class="col-fixed renk">RENK</div>
<div class="col-fixed ana">ÜRÜN ANA<br />GRUBU</div> <div class="col-fixed ana">ÜRÜN ANA GRUBU</div>
<div class="col-fixed alt">ÜRÜN ALT<br />GRUBU</div> <div class="col-fixed alt">ÜRÜN ALT GRUBU</div>
<div class="col-fixed aciklama-col">AÇIKLAMA</div> <div class="col-fixed aciklama-col">AÇIKLAMA</div>
<div class="beden-block"> <div class="beden-block">
@@ -289,7 +312,7 @@
<!-- ======================================================= <!-- =======================================================
🔹 GRID BODY (Final Stabil) + EDITOR aynı scrollda 🔹 GRID BODY (Final Stabil) + EDITOR aynı scrollda
======================================================== --> ======================================================== -->
<div class="order-scroll-y"> <!-- ✅ YENİ: Grid + Editor ortak dikey scroll --> <div class="order-scroll-y" :class="{ 'compact-grid-header': compactGridHeader }"> <!-- ✅ YENİ: Grid + Editor ortak dikey scroll -->
<div class="order-grid-body"> <div class="order-grid-body">
<template v-for="grp in groupedRows" :key="grp.name"> <template v-for="grp in groupedRows" :key="grp.name">
<div :class="['summary-group', grp.open ? 'open' : 'closed']"> <div :class="['summary-group', grp.open ? 'open' : 'closed']">
@@ -424,7 +447,24 @@
<!-- ======================================================= <!-- =======================================================
🔹 SATIR DÜZENLEYİCİ FORM (EDITOR) 🔹 SATIR DÜZENLEYİCİ FORM (EDITOR)
======================================================== --> ======================================================== -->
<div class="editor q-mt-lg q-pa-sm"> <q-dialog
v-model="showEditor"
class="order-editor-dialog"
:maximized="$q.screen.lt.md"
full-width
transition-show="jump-down"
transition-hide="jump-up"
persistent
>
<q-card class="order-editor-card">
<q-card-section class="row items-center justify-between">
<div class="text-subtitle1 text-weight-bold">
{{ isEditing ? 'Satır Düzenle' : 'Yeni Satır' }}
</div>
<q-btn flat round icon="close" @click="showEditor = false" />
</q-card-section>
<q-separator />
<q-card-section class="editor q-pa-sm">
<!-- 🔸 1. Satır: Model ve Ürün Bilgileri --> <!-- 🔸 1. Satır: Model ve Ürün Bilgileri -->
<div class="row q-col-gutter-sm q-mb-sm"> <div class="row q-col-gutter-sm q-mb-sm">
@@ -717,28 +757,11 @@
butonuna basarak işlemleri kaydedebilirsiniz. butonuna basarak işlemleri kaydedebilirsiniz.
</div> </div>
<!-- ======================================================= </q-card-section>
🔹 SİPARİŞ GENEL AÇIKLAMASI </q-card>
======================================================== --> </q-dialog>
<div class="row q-mt-md">
<div class="col-12">
<q-input
v-model="form.Description"
type="textarea"
label="Sipariş Genel ıklaması"
filled
dense
autogrow
maxlength="1500"
counter
placeholder="Siparişe genel ıklama giriniz (örn. teslimat, üretim notu, müşteri isteği...)"
:disable="isClosedRow"
/>
</div>
</div>
</div> <!-- editor -->
</div> <!-- ✅ order-scroll-y --> </div> <!-- ✅ order-scroll-y -->
</template>
</q-page> </q-page>
<q-page <q-page
@@ -777,6 +800,8 @@ const canExportOrder = canExport('order')
const formatDate = formatDateDisplay const formatDate = formatDateDisplay
const showEditor = ref(false)
const compactGridHeader = ref(false)
@@ -1094,6 +1119,16 @@ const seriMultiplier = ref(1)
const loadingHeader = ref(true) const loadingHeader = ref(true)
const loadingCari = ref(true) const loadingCari = ref(true)
const loadingModels = ref(true) const loadingModels = ref(true)
const isPageBlocking = computed(() => {
if (!isEditMode.value) return false
const headerReady = !!orderStore.header
return (
loadingHeader.value ||
loadingCari.value ||
loadingModels.value ||
!headerReady
)
})
/* =========================================================== /* ===========================================================
🔹 CARİ INFO STATE 🔹 CARİ INFO STATE
=========================================================== */ =========================================================== */
@@ -2346,11 +2381,22 @@ const editRow = async (row) => {
notify: true, notify: true,
loadSizes: true loadSizes: true
}) })
showEditor.value = true
} catch (err) { } catch (err) {
console.error('❌ editRow hata:', err) console.error('❌ editRow hata:', err)
} }
} }
const openNewRowEditor = async () => {
if (!hasRowMutationPermission()) {
notifyNoPermission('Siparis satiri ekleme/guncelleme yetkiniz yok')
return
}
await resetEditor(true)
showEditor.value = true
}
@@ -2765,6 +2811,7 @@ const onSaveOrUpdateRow = async () => {
stockMap, stockMap,
$q $q
}) })
showEditor.value = false
} }

View File

@@ -126,6 +126,13 @@
</q-icon> </q-icon>
</q-td> </q-td>
</template> </template>
<template #body-cell-HasUretimUrunu="props">
<q-td :props="props" class="text-left">
<span v-if="props.row.HasUretimUrunu" class="text-weight-bold text-negative">
ÜRETİME VERİLECEK ÜRÜNÜ VAR
</span>
</q-td>
</template>
<template #body-cell-OrderDate="props"> <template #body-cell-OrderDate="props">
<q-td :props="props" class="text-center"> <q-td :props="props" class="text-center">
@@ -366,6 +373,7 @@ const columns = [
format: val => Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' %' format: val => Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' %'
}, },
{ name: 'IsCreditableConfirmed', label: 'Durum', field: 'IsCreditableConfirmed', align: 'center', sortable: true }, { name: 'IsCreditableConfirmed', label: 'Durum', field: 'IsCreditableConfirmed', align: 'center', sortable: true },
{ name: 'HasUretimUrunu', label: 'Üretim', field: 'HasUretimUrunu', align: 'left', sortable: true, style: 'min-width:190px;white-space:nowrap', headerStyle: 'min-width:190px;white-space:nowrap' },
{ name: 'Description', label: 'Açıklama', field: 'Description', align: 'left', sortable: false, classes: 'ol-col-desc', headerClasses: 'ol-col-desc', style: 'width:160px;max-width:160px', headerStyle: 'width:160px;max-width:160px' }, { name: 'Description', label: 'Açıklama', field: 'Description', align: 'left', sortable: false, classes: 'ol-col-desc', headerClasses: 'ol-col-desc', style: 'width:160px;max-width:160px', headerStyle: 'width:160px;max-width:160px' },
{ name: 'pdf', label: 'PDF', field: 'pdf', align: 'center', sortable: false } { name: 'pdf', label: 'PDF', field: 'pdf', align: 'center', sortable: false }
] ]

View File

@@ -0,0 +1,463 @@
<template>
<q-page class="q-pa-md">
<div class="row items-center justify-between">
<div>
<div class="text-h6 text-weight-bold">Uretime Verilen Urunleri Guncelle</div>
<div class="text-caption text-grey-7 q-mt-xs">
OrderHeaderID: {{ orderHeaderID || '-' }}
</div>
</div>
<q-btn
color="primary"
icon="refresh"
label="Yenile"
:loading="store.loading"
@click="refreshAll"
/>
</div>
<div class="filter-bar row q-col-gutter-md q-mt-md">
<div class="col-5">
<q-input
:model-value="cariLabel"
label="Cari Secimi"
filled
dense
readonly
/>
</div>
<div class="col-3">
<q-input
v-model="descFilter"
label="Aciklama Ara"
filled
dense
clearable
/>
</div>
<div class="col-2">
<q-input
:model-value="header?.OrderNumber || ''"
label="Siparis No"
filled
dense
readonly
/>
</div>
<div class="col-2">
<q-input
:model-value="formatDate(header?.OrderDate)"
label="Olusturulma Tarihi"
filled
dense
readonly
/>
</div>
<div class="col-2">
<q-input
:model-value="formatDate(header?.AverageDueDate)"
label="Tahmini Termin Tarihi"
filled
dense
readonly
/>
</div>
</div>
<q-table
class="q-mt-md"
flat
bordered
dense
separator="cell"
row-key="RowKey"
:rows="filteredRows"
:columns="columns"
:loading="store.loading"
no-data-label="Uretime verilecek urun bulunamadi"
:rows-per-page-options="[0]"
hide-bottom
>
<template #body-cell-actions="props">
<q-td :props="props" class="text-center">
<q-btn
color="primary"
icon="save"
flat
round
dense
:loading="rowSavingId === props.row.RowKey"
@click="onRowSubmit(props.row)"
>
<q-tooltip>Satiri Guncelle</q-tooltip>
</q-btn>
</q-td>
</template>
<template #body-cell-NewItemCode="props">
<q-td :props="props">
<q-input
v-model="props.row.NewItemCode"
dense
filled
label="Yeni Urun"
@update:model-value="val => onNewItemChange(props.row, val)"
>
<template #append>
<q-icon name="arrow_drop_down" class="cursor-pointer" />
</template>
<q-menu
anchor="bottom left"
self="top left"
fit
>
<div class="q-pa-sm" style="min-width:260px">
<q-input
v-model="productSearch"
dense
filled
debounce="200"
placeholder="Urun ara..."
/>
<q-list class="q-mt-xs" bordered separator>
<q-item
v-for="opt in filteredProducts"
:key="opt.ProductCode"
clickable
@click="onSelectProduct(props.row, opt.ProductCode)"
>
<q-item-section>{{ opt.ProductCode }}</q-item-section>
</q-item>
</q-list>
</div>
</q-menu>
</q-input>
</q-td>
</template>
<template #body-cell-NewColor="props">
<q-td :props="props">
<q-select
v-model="props.row.NewColor"
:options="getColorOptions(props.row)"
option-label="colorLabel"
option-value="color_code"
emit-value
map-options
use-input
dense
filled
label="Yeni Renk"
@update:model-value="() => onNewColorChange(props.row)"
/>
</q-td>
</template>
<template #body-cell-NewDim2="props">
<q-td :props="props">
<q-select
v-model="props.row.NewDim2"
:options="getSecondColorOptions(props.row)"
option-label="item_dim2_code"
option-value="item_dim2_code"
emit-value
map-options
use-input
dense
filled
label="Yeni 2. Renk"
/>
</q-td>
</template>
<template #body-cell-NewDesc="props">
<q-td :props="props">
<q-input
v-model="props.row.NewDesc"
dense
filled
label="Yeni Aciklama"
/>
</q-td>
</template>
</q-table>
<q-banner v-if="store.error" class="bg-red text-white q-mt-sm">
Hata: {{ store.error }}
</q-banner>
</q-page>
</template>
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useQuasar } from 'quasar'
import { useOrderProductionItemStore } from 'src/stores/OrderProductionItemStore'
const route = useRoute()
const $q = useQuasar()
const store = useOrderProductionItemStore()
const orderHeaderID = computed(() => String(route.params.orderHeaderID || '').trim())
const header = computed(() => store.header || {})
const cariLabel = computed(() => {
const code = header.value?.CurrAccCode || ''
const name = header.value?.CurrAccDescription || ''
if (!code && !name) return ''
if (!name) return code
return `${code} - ${name}`
})
const rows = ref([])
const descFilter = ref('')
const productOptions = ref([])
const productSearch = ref('')
const rowSavingId = ref('')
const columns = [
{ name: 'OldItemCode', label: 'Eski Urun Kodu', field: 'OldItemCode', align: 'left', sortable: true, style: 'min-width:140px;white-space:nowrap', headerStyle: 'min-width:140px;white-space:nowrap' },
{ name: 'OldColor', label: 'Eski Urun Rengi', field: 'OldColor', align: 'left', sortable: true, style: 'min-width:120px;white-space:nowrap', headerStyle: 'min-width:120px;white-space:nowrap' },
{ name: 'OldDim2', label: 'Eski 2. Renk', field: 'OldDim2', align: 'left', sortable: true, style: 'min-width:110px;white-space:nowrap', headerStyle: 'min-width:110px;white-space:nowrap' },
{ name: 'OldDesc', label: 'Eski Aciklama', field: 'OldDesc', align: 'left', sortable: false, style: 'min-width:180px;white-space:nowrap', headerStyle: 'min-width:180px;white-space:nowrap' },
{ name: 'OldSizes', label: 'Bedenler', field: 'OldSizesLabel', align: 'left', sortable: false, style: 'min-width:160px;white-space:nowrap', headerStyle: 'min-width:160px;white-space:nowrap' },
{ name: 'NewItemCode', label: 'Yeni Urun Kodu', field: 'NewItemCode', align: 'left', sortable: false, style: 'min-width:190px;', headerStyle: 'min-width:190px;' },
{ name: 'NewColor', label: 'Yeni Urun Rengi', field: 'NewColor', align: 'left', sortable: false, style: 'min-width:160px;', headerStyle: 'min-width:160px;' },
{ name: 'NewDim2', label: 'Yeni 2. Renk', field: 'NewDim2', align: 'left', sortable: false, style: 'min-width:160px;', headerStyle: 'min-width:160px;' },
{ name: 'NewDesc', label: 'Yeni Aciklama', field: 'NewDesc', align: 'left', sortable: false, style: 'min-width:220px;', headerStyle: 'min-width:220px;' },
{ name: 'actions', label: '', field: 'actions', align: 'center', sortable: false, style: 'width:60px;', headerStyle: 'width:60px;' }
]
onMounted(async () => {
await refreshAll()
})
watch(orderHeaderID, async (id) => {
await refreshAll()
})
watch(
() => store.items,
(items) => {
rows.value = groupItems(items || [])
},
{ immediate: true }
)
watch(
() => store.products,
(products) => {
productOptions.value = products || []
},
{ immediate: true }
)
function formatDate (val) {
if (!val) return ''
const text = String(val)
return text.length >= 10 ? text.slice(0, 10) : text
}
const filteredProducts = computed(() => {
const needle = String(productSearch.value || '').toLowerCase()
if (!needle) return productOptions.value.slice(0, 50)
return productOptions.value.filter(p =>
String(p?.ProductCode || '').toLowerCase().includes(needle)
).slice(0, 50)
})
const filteredRows = computed(() => {
const needle = String(descFilter.value || '').toLowerCase().trim()
if (!needle) return rows.value
return rows.value.filter(r =>
String(r?.OldDesc || '').toLowerCase().includes(needle)
)
})
function onSelectProduct (row, code) {
productSearch.value = ''
onNewItemChange(row, code)
}
function onNewItemChange (row, val) {
const next = String(val || '').trim()
if (next && !isValidModelCode(next)) {
$q.notify({ type: 'negative', message: 'Model kodu formati gecersiz. Ornek: S000-DMY00001' })
row.NewItemCode = ''
row.NewColor = ''
row.NewDim2 = ''
return
}
row.NewItemCode = next ? next.toUpperCase() : ''
row.NewColor = ''
row.NewDim2 = ''
if (row.NewItemCode) {
store.fetchColors(row.NewItemCode)
}
}
function onNewColorChange (row) {
row.NewDim2 = ''
if (row.NewItemCode && row.NewColor) {
store.fetchSecondColors(row.NewItemCode, row.NewColor)
}
}
function getColorOptions (row) {
const code = row?.NewItemCode || ''
const list = store.colorOptionsByCode[code] || []
return list.map(c => ({
...c,
colorLabel: `${c.color_code} - ${c.color_description || ''}`.trim()
}))
}
function getSecondColorOptions (row) {
const code = row?.NewItemCode || ''
const color = row?.NewColor || ''
const key = `${code}::${color}`
return store.secondColorOptionsByKey[key] || []
}
function isValidModelCode (value) {
const text = String(value || '').trim().toUpperCase()
return /^[A-Z][0-9]{3}-[A-Z]{3}[0-9]{5}$/.test(text)
}
function buildGroupKey (item) {
const parts = [
String(item?.OldItemCode || '').trim(),
String(item?.OldColor || '').trim(),
String(item?.OldDim2 || '').trim(),
String(item?.OldDesc || '').trim(),
String(item?.OldDim3 || '').trim()
]
return parts.join('||')
}
function formatSizes (sizeMap) {
const entries = Object.entries(sizeMap || {})
if (!entries.length) return { list: [], label: '-' }
entries.sort((a, b) => String(a[0]).localeCompare(String(b[0])))
const label = entries.map(([k, v]) => (v > 1 ? `${k}(${v})` : k)).join(', ')
return { list: entries.map(([k]) => k), label }
}
function groupItems (items) {
const map = new Map()
for (const it of items) {
const key = buildGroupKey(it)
if (!map.has(key)) {
map.set(key, {
RowKey: key,
OrderHeaderID: it.OrderHeaderID,
OldItemCode: it.OldItemCode,
OldColor: it.OldColor,
OldDim2: it.OldDim2,
OldDim3: it.OldDim3,
OldDesc: it.OldDesc,
OrderLineIDs: [],
OldSizes: [],
OldSizesLabel: '',
NewItemCode: '',
NewColor: '',
NewDim2: '',
NewDesc: '',
IsVariantMissing: !!it.IsVariantMissing
})
}
const g = map.get(key)
if (it?.OrderLineID) g.OrderLineIDs.push(it.OrderLineID)
const size = String(it?.OldDim1 || '').trim()
if (size !== '') {
g.__sizeMap = g.__sizeMap || {}
g.__sizeMap[size] = (g.__sizeMap[size] || 0) + 1
}
if (it?.IsVariantMissing) g.IsVariantMissing = true
}
const out = []
for (const g of map.values()) {
const sizes = formatSizes(g.__sizeMap || {})
g.OldSizes = sizes.list
g.OldSizesLabel = sizes.label
delete g.__sizeMap
out.push(g)
}
return out
}
function buildPayloadLines () {
return rows.value.flatMap(r =>
(r.OrderLineIDs || []).map(id => ({
OrderLineID: id,
NewItemCode: String(r.NewItemCode || '').trim(),
NewColor: String(r.NewColor || '').trim(),
NewDim2: String(r.NewDim2 || '').trim(),
NewDesc: String(r.NewDesc || '').trim()
}))
)
}
async function refreshAll () {
await store.fetchHeader(orderHeaderID.value)
await store.fetchItems(orderHeaderID.value)
await store.fetchProducts()
}
async function onRowSubmit (row) {
const baseLine = {
NewItemCode: String(row.NewItemCode || '').trim(),
NewColor: String(row.NewColor || '').trim(),
NewDim2: String(row.NewDim2 || '').trim(),
NewDesc: String(row.NewDesc || '').trim()
}
if (!baseLine.NewItemCode || !baseLine.NewColor) {
$q.notify({ type: 'negative', message: 'Yeni urun ve renk zorunludur.' })
return
}
const lines = (row.OrderLineIDs || []).map(id => ({
OrderLineID: id,
...baseLine
}))
if (!lines.length) {
$q.notify({ type: 'negative', message: 'Satir bulunamadi.' })
return
}
rowSavingId.value = row.RowKey
try {
const validate = await store.validateUpdates(orderHeaderID.value, lines)
const missingCount = validate?.missingCount || 0
if (missingCount > 0) {
const missingList = (validate?.missing || []).map(v => (
`${v.ItemCode} / ${v.ColorCode} / ${v.ItemDim1Code} / ${v.ItemDim2Code}`
))
$q.dialog({
title: 'Eksik Varyantlar',
message: `Eksik varyant bulundu: ${missingCount}<br><br>${missingList.join('<br>')}`,
html: true,
ok: { label: 'Ekle ve Guncelle', color: 'primary' },
cancel: { label: 'Vazgec', flat: true }
}).onOk(async () => {
await store.applyUpdates(orderHeaderID.value, lines, true)
await store.fetchItems(orderHeaderID.value)
})
return
}
await store.applyUpdates(orderHeaderID.value, lines, false)
await store.fetchItems(orderHeaderID.value)
} catch (err) {
$q.notify({ type: 'negative', message: 'Islem basarisiz.' })
} finally {
rowSavingId.value = ''
}
}
</script>

View File

@@ -0,0 +1,357 @@
<template>
<q-page v-if="canReadOrder" class="ol-page">
<div class="ol-filter-bar">
<div class="ol-filter-row">
<q-input
v-model="store.filters.search"
class="ol-filter-input ol-search"
dense
filled
debounce="300"
clearable
label="Arama (Sipariş No / Cari / Açıklama)"
>
<template #append>
<q-icon name="search" />
</template>
</q-input>
<q-input
v-model="store.filters.CurrAccCode"
class="ol-filter-input"
dense
filled
clearable
label="Cari Kodu"
/>
<q-input
v-model="store.filters.OrderDate"
class="ol-filter-input"
dense
filled
type="date"
label="Sipariş Tarihi"
/>
<div class="ol-filter-actions">
<q-btn
label="Temizle"
icon="clear"
color="grey-7"
flat
:disable="store.loading"
@click="clearFilters"
>
<q-tooltip>Tüm filtreleri temizle</q-tooltip>
</q-btn>
<q-btn
label="Yenile"
color="primary"
icon="refresh"
:loading="store.loading"
@click="store.fetchOrders"
/>
<q-btn
label="Excel'e Aktar"
icon="download"
color="primary"
outline
:disable="store.loading || productionOrders.length === 0"
@click="exportExcel"
/>
</div>
<div class="ol-filter-total">
<div class="ol-total-line">
<span class="ol-total-label">Toplam USD:</span>
<strong class="ol-total-value">
{{ totalVisibleUSD.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
</strong>
</div>
</div>
</div>
</div>
<q-table
title="Üretime Verilecek Ürünleri Olan Siparişler"
class="ol-table"
flat
bordered
dense
separator="cell"
row-key="OrderHeaderID"
:rows="productionOrders"
:columns="columns"
:loading="store.loading"
no-data-label="Üretime verilecek sipariş bulunamadı"
:rows-per-page-options="[0]"
hide-bottom
>
<template #body-cell-open="props">
<q-td :props="props" class="text-center">
<q-btn
icon="open_in_new"
color="primary"
flat
round
dense
@click="selectOrder(props.row)"
>
<q-tooltip>Siparişi </q-tooltip>
</q-btn>
</q-td>
</template>
<template #body-cell-IsCreditableConfirmed="props">
<q-td :props="props" class="text-center q-gutter-sm">
<q-btn
icon="picture_as_pdf"
color="red"
flat
round
dense
@click="printPDF(props.row)"
>
<q-tooltip>Siparişi PDF olarak </q-tooltip>
</q-btn>
<q-icon
:name="props.row.IsCreditableConfirmed ? 'check_circle' : 'cancel'"
:color="props.row.IsCreditableConfirmed ? 'green' : 'red'"
size="20px"
>
<q-tooltip>
{{ props.row.IsCreditableConfirmed ? 'Onaylı' : 'Onaysız' }}
</q-tooltip>
</q-icon>
</q-td>
</template>
<template #body-cell-OrderDate="props">
<q-td :props="props" class="text-center">
{{ formatDate(props.row.OrderDate) }}
</q-td>
</template>
<template #body-cell-CreditableConfirmedDate="props">
<q-td :props="props" class="text-center">
{{ formatDate(props.row.CreditableConfirmedDate) }}
</q-td>
</template>
<template #body-cell-CurrAccDescription="props">
<q-td :props="props" class="ol-col-cari">
<div class="ol-col-multiline">{{ props.value }}</div>
<q-tooltip v-if="props.value">
{{ props.value }}
</q-tooltip>
</q-td>
</template>
<template #body-cell-MusteriTemsilcisi="props">
<q-td :props="props" class="ol-col-short">
<div class="ol-col-multiline">{{ props.value }}</div>
<q-tooltip v-if="props.value">
{{ props.value }}
</q-tooltip>
</q-td>
</template>
<template #body-cell-Piyasa="props">
<q-td :props="props" class="ol-col-short">
<div class="ol-col-multiline">{{ props.value }}</div>
<q-tooltip v-if="props.value">
{{ props.value }}
</q-tooltip>
</q-td>
</template>
<template #body-cell-Description="props">
<q-td :props="props" class="ol-col-desc">
<div class="ol-col-multiline">{{ props.value }}</div>
<q-tooltip v-if="props.value">
{{ props.value }}
</q-tooltip>
</q-td>
</template>
<template #body-cell-HasUretimUrunu="props">
<q-td :props="props" class="text-center">
<q-icon
:name="props.row.HasUretimUrunu ? 'check_circle' : 'cancel'"
:color="props.row.HasUretimUrunu ? 'green' : 'grey-5'"
size="18px"
/>
</q-td>
</template>
</q-table>
<q-banner v-if="store.error" class="bg-red text-white q-mt-sm">
Hata: {{ store.error }}
</q-banner>
</q-page>
<q-page v-else class="q-pa-md flex flex-center">
<div class="text-negative text-subtitle1">
Bu modüle erişim yetkiniz yok.
</div>
</q-page>
</template>
<script setup>
import { computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useQuasar } from 'quasar'
import { useOrderProductionUpdateStore } from 'src/stores/OrderProductionUpdateStore'
import { useAuthStore } from 'src/stores/authStore'
import { usePermission } from 'src/composables/usePermission'
import api, { extractApiErrorDetail } from 'src/services/api'
const { canRead } = usePermission()
const canReadOrder = canRead('order')
const router = useRouter()
const $q = useQuasar()
const store = useOrderProductionUpdateStore()
let searchTimer = null
watch(
() => store.filters.search,
() => {
clearTimeout(searchTimer)
searchTimer = setTimeout(() => {
store.fetchOrders()
}, 400)
}
)
const productionOrders = computed(() => store.filteredOrders)
const totalVisibleUSD = computed(() =>
productionOrders.value.reduce((sum, o) => {
const v = Number(o.TotalAmountUSD || 0)
return sum + (Number.isFinite(v) ? v : 0)
}, 0)
)
function clearFilters () {
store.filters.search = ''
store.filters.CurrAccCode = ''
store.filters.OrderDate = ''
store.fetchOrders()
}
function formatDate (v) {
if (!v) return ''
return String(v)
}
function selectOrder (row) {
if (!row?.OrderHeaderID) {
$q.notify({ type: 'warning', message: 'OrderHeaderID bulunamadı' })
return
}
router.push({
name: 'orderproductionupdate',
params: { orderHeaderID: row.OrderHeaderID }
})
}
async function printPDF (row) {
try {
const auth = useAuthStore()
if (!auth?.token) {
$q.notify({ type: 'warning', message: 'Oturum bulunamadı' })
return
}
const res = await api.get(`/order/pdf/${row.OrderHeaderID}`, {
responseType: 'blob'
})
const blob = new Blob([res.data], { type: 'application/pdf' })
const url = URL.createObjectURL(blob)
window.open(url, '_blank')
setTimeout(() => URL.revokeObjectURL(url), 2000)
} catch (err) {
const status = err?.response?.status
const detail = extractApiErrorDetail(err)
console.error(`PDF load error [${status}] /order/pdf/${row?.OrderHeaderID}: ${detail}`)
$q.notify({ type: 'negative', message: `PDF alınamadı: ${detail}` })
}
}
function exportExcel () {
const auth = useAuthStore()
if (!auth?.token) {
$q.notify({
type: 'negative',
message: 'Oturum bulunamadı'
})
return
}
const params = new URLSearchParams()
if (store.filters.search) params.append('search', store.filters.search)
api.get(`/orders/production-list?${params.toString()}`, {
responseType: 'blob'
}).then((res) => {
const blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'uretime_verilecek_siparisler.xlsx'
a.click()
URL.revokeObjectURL(url)
}).catch((err) => {
const detail = extractApiErrorDetail(err)
$q.notify({ type: 'negative', message: `Excel alınamadı: ${detail}` })
})
}
onMounted(() => {
store.fetchOrders()
})
const columns = [
{ name: 'open', label: '', field: 'open', align: 'center', sortable: false },
{ name: 'OrderNumber', label: 'Sipariş No', field: 'OrderNumber', align: 'left', sortable: true, style: 'min-width:108px;white-space:nowrap', headerStyle: 'min-width:108px;white-space:nowrap' },
{ name: 'OrderDate', label: 'Tarih', field: 'OrderDate', align: 'center', sortable: true, style: 'min-width:82px;white-space:nowrap', headerStyle: 'min-width:82px;white-space:nowrap' },
{ name: 'CurrAccCode', label: 'Cari Kod', field: 'CurrAccCode', align: 'left', sortable: true, style: 'min-width:82px;white-space:nowrap', headerStyle: 'min-width:82px;white-space:nowrap' },
{ name: 'CurrAccDescription', label: 'Cari Adı', field: 'CurrAccDescription', align: 'left', sortable: true, classes: 'ol-col-cari', headerClasses: 'ol-col-cari', style: 'width:160px;max-width:160px', headerStyle: 'width:160px;max-width:160px' },
{ name: 'MusteriTemsilcisi', label: 'Temsilci', field: 'MusteriTemsilcisi', align: 'left', sortable: true, classes: 'ol-col-short', headerClasses: 'ol-col-short', style: 'width:88px;max-width:88px', headerStyle: 'width:88px;max-width:88px' },
{ name: 'Piyasa', label: 'Piyasa', field: 'Piyasa', align: 'left', sortable: true, classes: 'ol-col-short', headerClasses: 'ol-col-short', style: 'width:72px;max-width:72px', headerStyle: 'width:72px;max-width:72px' },
{ name: 'CreditableConfirmedDate', label: 'Onay', field: 'CreditableConfirmedDate', align: 'center', sortable: true, style: 'min-width:86px;white-space:nowrap', headerStyle: 'min-width:86px;white-space:nowrap' },
{ name: 'DocCurrencyCode', label: 'PB', field: 'DocCurrencyCode', align: 'center', sortable: true, style: 'min-width:46px;white-space:nowrap', headerStyle: 'min-width:46px;white-space:nowrap' },
{
name: 'TotalAmount',
label: 'Tutar',
field: 'TotalAmount',
align: 'right',
sortable: true,
style: 'min-width:120px;white-space:nowrap',
headerStyle: 'min-width:120px;white-space:nowrap',
format: (val, row) => Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' ' + row.DocCurrencyCode
},
{
name: 'TotalAmountUSD',
label: 'Tutar (USD)',
field: 'TotalAmountUSD',
align: 'right',
sortable: true,
style: 'min-width:120px;white-space:nowrap',
headerStyle: 'min-width:120px;white-space:nowrap',
format: val => Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' USD'
},
{ name: 'IsCreditableConfirmed', label: 'Durum', field: 'IsCreditableConfirmed', align: 'center', sortable: true },
{ name: 'HasUretimUrunu', label: 'Üretim', field: 'HasUretimUrunu', align: 'left', sortable: true, style: 'min-width:190px;white-space:nowrap', headerStyle: 'min-width:190px;white-space:nowrap' },
{ name: 'Description', label: 'Açıklama', field: 'Description', align: 'left', sortable: false, classes: 'ol-col-desc', headerClasses: 'ol-col-desc', style: 'width:160px;max-width:160px', headerStyle: 'width:160px;max-width:160px' }
]
</script>

View File

@@ -46,6 +46,9 @@
<q-item clickable @click="selectAllModules"> <q-item clickable @click="selectAllModules">
<q-item-section>Tümünü Seç</q-item-section> <q-item-section>Tümünü Seç</q-item-section>
</q-item> </q-item>
<q-item clickable @click="clearAllModules">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator /> <q-separator />
<q-item <q-item
v-for="m in store.modules" v-for="m in store.modules"
@@ -78,6 +81,9 @@
<q-item clickable @click="selectAllActionsForActive"> <q-item clickable @click="selectAllActionsForActive">
<q-item-section>Tümünü Seç</q-item-section> <q-item-section>Tümünü Seç</q-item-section>
</q-item> </q-item>
<q-item clickable @click="clearAllActionsForActive">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator /> <q-separator />
<q-item <q-item
v-for="a in actionsForActiveModule" v-for="a in actionsForActiveModule"
@@ -180,6 +186,7 @@ const canUpdateUser = canUpdate('user')
const selectedModules = ref([]) const selectedModules = ref([])
const selectedActionsByModule = ref({}) const selectedActionsByModule = ref({})
const activeModuleCode = ref('') const activeModuleCode = ref('')
const allowEmptySelection = ref(false)
const actionLabelMap = { const actionLabelMap = {
update: 'Güncelleme', update: 'Güncelleme',
@@ -284,9 +291,15 @@ function syncSelections () {
} }
const selected = selectedModules.value.filter((m) => availableModules.includes(m)) const selected = selectedModules.value.filter((m) => availableModules.includes(m))
selectedModules.value = selected.length ? selected : [...availableModules] if (selected.length) {
selectedModules.value = selected
} else {
selectedModules.value = allowEmptySelection.value ? [] : [...availableModules]
}
if (!selectedModules.value.includes(activeModuleCode.value)) { if (!selectedModules.value.length) {
activeModuleCode.value = ''
} else if (!selectedModules.value.includes(activeModuleCode.value)) {
activeModuleCode.value = selectedModules.value[0] activeModuleCode.value = selectedModules.value[0]
} }
@@ -295,7 +308,7 @@ function syncSelections () {
const allActions = actionsByModule.value[m] || [] const allActions = actionsByModule.value[m] || []
const prev = selectedActionsByModule.value[m] || [] const prev = selectedActionsByModule.value[m] || []
const filtered = prev.filter((a) => allActions.includes(a)) const filtered = prev.filter((a) => allActions.includes(a))
next[m] = filtered.length ? filtered : [...allActions] next[m] = filtered.length ? filtered : (allowEmptySelection.value ? [] : [...allActions])
}) })
selectedActionsByModule.value = next selectedActionsByModule.value = next
} }
@@ -313,6 +326,7 @@ function isModuleSelected (moduleCode) {
} }
function toggleModule (moduleCode, checked) { function toggleModule (moduleCode, checked) {
allowEmptySelection.value = false
const set = new Set(selectedModules.value) const set = new Set(selectedModules.value)
if (checked) { if (checked) {
set.add(moduleCode) set.add(moduleCode)
@@ -321,9 +335,8 @@ function toggleModule (moduleCode, checked) {
} }
selectedModules.value = [...set] selectedModules.value = [...set]
if (!selectedModules.value.length) { if (!selectedModules.value.length) {
selectedModules.value = [moduleCode] activeModuleCode.value = ''
} } else if (!selectedModules.value.includes(activeModuleCode.value)) {
if (!selectedModules.value.includes(activeModuleCode.value)) {
activeModuleCode.value = selectedModules.value[0] activeModuleCode.value = selectedModules.value[0]
} }
syncSelections() syncSelections()
@@ -334,24 +347,31 @@ function onModuleRowClick (moduleCode) {
} }
function selectAllModules () { function selectAllModules () {
allowEmptySelection.value = false
selectedModules.value = (store.modules || []).map((m) => m.value) selectedModules.value = (store.modules || []).map((m) => m.value)
syncSelections() syncSelections()
} }
function clearAllModules () {
allowEmptySelection.value = true
selectedModules.value = []
selectedActionsByModule.value = {}
activeModuleCode.value = ''
syncSelections()
}
function isActionSelected (moduleCode, action) { function isActionSelected (moduleCode, action) {
return (selectedActionsByModule.value[moduleCode] || []).includes(action) return (selectedActionsByModule.value[moduleCode] || []).includes(action)
} }
function toggleAction (moduleCode, action, checked) { function toggleAction (moduleCode, action, checked) {
allowEmptySelection.value = false
const current = new Set(selectedActionsByModule.value[moduleCode] || []) const current = new Set(selectedActionsByModule.value[moduleCode] || [])
if (checked) { if (checked) {
current.add(action) current.add(action)
} else { } else {
current.delete(action) current.delete(action)
} }
if (current.size === 0) {
current.add(action)
}
selectedActionsByModule.value = { selectedActionsByModule.value = {
...selectedActionsByModule.value, ...selectedActionsByModule.value,
[moduleCode]: [...current] [moduleCode]: [...current]
@@ -359,6 +379,7 @@ function toggleAction (moduleCode, action, checked) {
} }
function selectAllActionsForActive () { function selectAllActionsForActive () {
allowEmptySelection.value = false
if (!activeModuleCode.value) return if (!activeModuleCode.value) return
selectedActionsByModule.value = { selectedActionsByModule.value = {
...selectedActionsByModule.value, ...selectedActionsByModule.value,
@@ -366,6 +387,15 @@ function selectAllActionsForActive () {
} }
} }
function clearAllActionsForActive () {
allowEmptySelection.value = true
if (!activeModuleCode.value) return
selectedActionsByModule.value = {
...selectedActionsByModule.value,
[activeModuleCode.value]: []
}
}
const permissionColumns = computed(() => { const permissionColumns = computed(() => {
const cols = [] const cols = []
selectedModules.value.forEach((m) => { selectedModules.value.forEach((m) => {

View File

@@ -224,6 +224,15 @@
filled filled
behavior="menu" behavior="menu"
> >
<template #before-options>
<q-item clickable @click="selectAllPiyasalar">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable @click="clearPiyasalar">
<q-item-section>Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #selected-item="scope"> <template #selected-item="scope">
<q-chip <q-chip
removable removable
@@ -240,6 +249,8 @@
<q-checkbox <q-checkbox
:model-value="scope.selected" :model-value="scope.selected"
tabindex="-1" tabindex="-1"
@update:model-value="() => scope.toggleOption(scope.opt)"
@click.stop
/> />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
@@ -352,6 +363,16 @@ const canSendPasswordMail = computed(() => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((form.value.email || '').trim()) return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((form.value.email || '').trim())
}) })
function selectAllPiyasalar () {
form.value.piyasalar = (piyasaOptions.value || [])
.map((o) => o.value)
.filter(Boolean)
}
function clearPiyasalar () {
form.value.piyasalar = []
}
/* ================= LIFECYCLE ================= */ /* ================= LIFECYCLE ================= */
watch( watch(
() => userId.value, () => userId.value,

View File

@@ -1,5 +1,5 @@
<template> <template>
<q-page v-if="canReadFinance" class="q-pa-md page-col"> <q-page v-if="canReadFinance" class="q-pa-md page-col statement-page">
<!-- 🔹 Cari Kod / İsim (sabit) --> <!-- 🔹 Cari Kod / İsim (sabit) -->
<div class="filter-sticky"> <div class="filter-sticky">
@@ -47,7 +47,12 @@
<template #append> <template #append>
<q-icon name="event" class="cursor-pointer"> <q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale"> <q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-date v-model="dateFrom" mask="YYYY-MM-DD" locale="tr-TR"/> <q-date
v-model="dateFrom"
mask="YYYY-MM-DD"
locale="tr-TR"
:options="isValidFromDate"
/>
</q-popup-proxy> </q-popup-proxy>
</q-icon> </q-icon>
</template> </template>
@@ -63,7 +68,12 @@
<template #append> <template #append>
<q-icon name="event" class="cursor-pointer"> <q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale"> <q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-date v-model="dateTo" mask="YYYY-MM-DD" locale="tr-TR" /> <q-date
v-model="dateTo"
mask="YYYY-MM-DD"
locale="tr-TR"
:options="isValidToDate"
/>
</q-popup-proxy> </q-popup-proxy>
</q-icon> </q-icon>
</template> </template>
@@ -163,19 +173,21 @@
<!-- Ana Tablo --> <!-- Ana Tablo -->
<q-table <q-table
class="sticky-table" class="sticky-table statement-table"
title="Hareketler" title="Hareketler"
:rows="statementheaderStore.groupedRows" :rows="statementheaderStore.groupedRows"
:columns="columns" :columns="columns"
:visible-columns="visibleColumns" :visible-columns="visibleColumns"
:row-key="row => row.OrderHeaderID + '_' + row.OrderNumber" :row-key="rowKeyFn"
flat flat
bordered bordered
dense dense
hide-bottom
wrap-cells
:rows-per-page-options="[0]" :rows-per-page-options="[0]"
:loading="statementheaderStore.loading" :loading="statementheaderStore.loading"
:table-style="{ tableLayout: 'auto', minWidth: '1600px' }" :table-style="{ tableLayout: 'fixed', width: '100%' }"
> >
<template #body="props"> <template #body="props">
@@ -332,6 +344,29 @@ onMounted(async () => {
const dateFrom = ref(dayjs().startOf('year').format('YYYY-MM-DD')) const dateFrom = ref(dayjs().startOf('year').format('YYYY-MM-DD'))
const dateTo = ref(dayjs().format('YYYY-MM-DD')) const dateTo = ref(dayjs().format('YYYY-MM-DD'))
function isValidFromDate (date) {
if (!dateTo.value) return true
return !dayjs(date).isAfter(dayjs(dateTo.value), 'day')
}
function isValidToDate (date) {
if (!dateFrom.value) return true
return !dayjs(date).isBefore(dayjs(dateFrom.value), 'day')
}
function hasInvalidDateRange () {
if (!dateFrom.value || !dateTo.value) return false
return dayjs(dateFrom.value).isAfter(dayjs(dateTo.value), 'day')
}
function notifyInvalidDateRange () {
$q.notify({
type: 'warning',
message: '⚠️ Başlangıç tarihi bitiş tarihinden sonra olamaz.',
position: 'top-right'
})
}
/* Parasal İşlem Tipi */ /* Parasal İşlem Tipi */
const monetaryTypeOptions = [ const monetaryTypeOptions = [
{ label: '1-2 hesap', value: ['1', '2'] }, { label: '1-2 hesap', value: ['1', '2'] },
@@ -373,6 +408,11 @@ async function onFilterClick() {
return return
} }
if (hasInvalidDateRange()) {
notifyInvalidDateRange()
return
}
await statementheaderStore.loadStatements({ await statementheaderStore.loadStatements({
startdate: dateFrom.value, startdate: dateFrom.value,
enddate: dateTo.value, enddate: dateTo.value,
@@ -483,6 +523,11 @@ async function handleDownload() {
return return
} }
if (hasInvalidDateRange()) {
notifyInvalidDateRange()
return
}
// ✅ Seçilen parasal işlem tipini gönder // ✅ Seçilen parasal işlem tipini gönder
const result = await downloadstpdfStore.downloadPDF( const result = await downloadstpdfStore.downloadPDF(
selectedCari.value, // accountCode selectedCari.value, // accountCode
@@ -524,6 +569,11 @@ async function CurrheadDownload() {
return return
} }
if (hasInvalidDateRange()) {
notifyInvalidDateRange()
return
}
// ✅ Yeni store fonksiyonu doğru şekilde çağrılıyor // ✅ Yeni store fonksiyonu doğru şekilde çağrılıyor
const result = await downloadstHeadStore.handlestHeadDownload( const result = await downloadstHeadStore.handlestHeadDownload(
selectedCari.value, // accountCode selectedCari.value, // accountCode
@@ -542,3 +592,100 @@ async function CurrheadDownload() {
} }
</script> </script>
<style scoped>
.statement-page {
height: calc(100vh - 56px);
display: flex;
flex-direction: column;
overflow: hidden;
}
.table-scroll {
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.sticky-bar {
position: sticky;
top: 0;
z-index: 20;
flex: 0 0 auto;
}
.statement-table {
flex: 1;
min-height: 0;
}
.statement-table :deep(.q-table__container) {
height: 100%;
display: flex;
flex-direction: column;
}
.statement-table :deep(.q-table__top) {
flex: 0 0 auto;
}
.statement-table :deep(.q-table__middle) {
flex: 1 1 auto;
min-height: 0;
overflow: auto !important;
max-height: none !important;
}
.statement-table :deep(thead th) {
position: sticky;
top: 0;
z-index: 10;
background: #fff;
}
.statement-table :deep(th),
.statement-table :deep(td) {
padding: 3px 6px !important;
font-size: 11px !important;
line-height: 1.2 !important;
}
.statement-table :deep(td) {
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
max-width: 120px;
}
.statement-table :deep(td[data-col="aciklama"]),
.statement-table :deep(th[data-col="aciklama"]) {
max-width: 220px;
}
.statement-table :deep(.resizable-cell-content) {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
white-space: normal !important;
}
@media (max-width: 1366px) {
.statement-table :deep(th),
.statement-table :deep(td) {
font-size: 10px !important;
padding: 2px 4px !important;
}
.statement-table :deep(td) {
max-width: 100px;
}
.statement-table :deep(td[data-col="aciklama"]),
.statement-table :deep(th[data-col="aciklama"]) {
max-width: 180px;
}
}
</style>

View File

@@ -14,6 +14,9 @@ export default route(function () {
routes routes
}) })
if (typeof window !== 'undefined' && process.env.DEV) {
window.__router = router
}
/* ============================================================ /* ============================================================
🔐 GLOBAL GUARD 🔐 GLOBAL GUARD
@@ -23,6 +26,17 @@ export default route(function () {
const auth = useAuthStore() const auth = useAuthStore()
const perm = usePermissionStore() const perm = usePermissionStore()
if (typeof window !== 'undefined') {
console.warn('🧭 ROUTE GUARD HIT:', {
path: to.fullPath,
meta: to.meta
})
}
if (typeof window !== 'undefined' && process.env.DEV) {
window.__auth = auth
window.__perm = perm
}
/* ================= PUBLIC ================= */ /* ================= PUBLIC ================= */

View File

@@ -69,7 +69,7 @@ const routes = [
path: '', path: '',
name: 'dashboard', name: 'dashboard',
component: () => import('pages/Dashboard.vue'), component: () => import('pages/Dashboard.vue'),
meta: { permission: 'system:read' } meta: {}
}, },
@@ -86,28 +86,28 @@ const routes = [
path: 'role-dept-permissions', path: 'role-dept-permissions',
name: 'role-dept-permissions', name: 'role-dept-permissions',
component: () => import('pages/RoleDepartmentPermissionGateway.vue'), component: () => import('pages/RoleDepartmentPermissionGateway.vue'),
meta: { permission: 'user:update' } meta: { permission: 'system:update' }
}, },
{ {
path: 'role-dept-permissions/list', path: 'role-dept-permissions/list',
name: 'role-dept-permissions-list', name: 'role-dept-permissions-list',
component: () => import('pages/RoleDepartmentPermissionList.vue'), component: () => import('pages/RoleDepartmentPermissionList.vue'),
meta: { permission: 'user:update' } meta: { permission: 'system:update' }
}, },
{ {
path: 'role-dept-permissions/editor', path: 'role-dept-permissions/editor',
name: 'role-dept-permissions-editor', name: 'role-dept-permissions-editor',
component: () => import('pages/RoleDepartmentPermissionPage.vue'), component: () => import('pages/RoleDepartmentPermissionPage.vue'),
meta: { permission: 'user:update' } meta: { permission: 'system:update' }
}, },
{ {
path: 'user-permissions', path: 'user-permissions',
name: 'user-permissions', name: 'user-permissions',
component: () => import('pages/UserPermissionPage.vue'), component: () => import('pages/UserPermissionPage.vue'),
meta: { permission: 'user:update' } meta: { permission: 'system:update' }
}, },
@@ -190,7 +190,7 @@ const routes = [
path: 'activity-logs', path: 'activity-logs',
name: 'activity-logs', name: 'activity-logs',
component: () => import('pages/ActivityLogs.vue'), component: () => import('pages/ActivityLogs.vue'),
meta: { permission: 'user:view' } meta: { permission: 'system:read' }
}, },
@@ -200,7 +200,7 @@ const routes = [
path: 'test-mail', path: 'test-mail',
name: 'test-mail', name: 'test-mail',
component: () => import('pages/TestMail.vue'), component: () => import('pages/TestMail.vue'),
meta: { permission: 'user:insert' } meta: { permission: 'system:update' }
}, },
@@ -242,6 +242,20 @@ const routes = [
meta: { permission: 'order:view' } meta: { permission: 'order:view' }
}, },
{
path: 'orderproductionupdate',
name: 'orderproductionupdate-list',
component: () => import('pages/OrderProductionUpdateList.vue'),
meta: { permission: 'order:update' }
},
{
path: 'orderproductionupdate/:orderHeaderID',
name: 'orderproductionupdate',
component: () => import('pages/OrderProductionUpdate.vue'),
props: true,
meta: { permission: 'order:update' }
},
{ {
path: 'order-bulk-close', path: 'order-bulk-close',
name: 'order-bulk-close', name: 'order-bulk-close',

View File

@@ -2,7 +2,10 @@ import axios from 'axios'
import qs from 'qs' import qs from 'qs'
import { useAuthStore } from 'stores/authStore' import { useAuthStore } from 'stores/authStore'
export const API_BASE_URL = '/api' const rawBaseUrl =
(typeof process !== 'undefined' && process.env?.VITE_API_BASE_URL) || '/api'
export const API_BASE_URL = String(rawBaseUrl).trim().replace(/\/+$/, '')
const AUTH_REFRESH_PATH = '/auth/refresh' const AUTH_REFRESH_PATH = '/auth/refresh'
const api = axios.create({ const api = axios.create({

View File

@@ -0,0 +1,147 @@
// src/stores/OrderProductionItemStore.js
import { defineStore } from 'pinia'
import api from 'src/services/api'
export const useOrderProductionItemStore = defineStore('orderproductionitems', {
state: () => ({
items: [],
header: null,
products: [],
colorOptionsByCode: {},
secondColorOptionsByKey: {},
loading: false,
saving: false,
error: null
}),
actions: {
async fetchHeader (orderHeaderID) {
if (!orderHeaderID) {
this.header = null
return
}
this.loading = true
this.error = null
try {
const res = await api.get(`/order/get/${encodeURIComponent(orderHeaderID)}`)
this.header = res?.data?.header || null
} catch (err) {
this.header = null
this.error = err?.response?.data || err?.message || 'Siparis bilgisi alinamadi'
} finally {
this.loading = false
}
},
async fetchItems (orderHeaderID) {
if (!orderHeaderID) {
this.items = []
return
}
this.loading = true
this.error = null
try {
const res = await api.get(`/orders/production-items/${encodeURIComponent(orderHeaderID)}`)
const data = res?.data
this.items = Array.isArray(data) ? data : []
} catch (err) {
this.items = []
this.error = err?.response?.data || err?.message || 'Liste alinamadi'
} finally {
this.loading = false
}
},
async fetchProducts () {
this.error = null
try {
const res = await api.get('/products')
const data = res?.data
this.products = Array.isArray(data) ? data : []
} catch (err) {
this.products = []
this.error = err?.response?.data || err?.message || 'Urun listesi alinamadi'
}
},
async fetchColors (productCode) {
const code = String(productCode || '').trim()
if (!code) return []
if (this.colorOptionsByCode[code]) {
return this.colorOptionsByCode[code]
}
try {
const res = await api.get('/product-colors', { params: { code } })
const data = res?.data
const list = Array.isArray(data) ? data : []
this.colorOptionsByCode[code] = list
return list
} catch (err) {
this.error = err?.response?.data || err?.message || 'Renk listesi alinamadi'
return []
}
},
async fetchSecondColors (productCode, colorCode) {
const code = String(productCode || '').trim()
const color = String(colorCode || '').trim()
if (!code || !color) return []
const key = `${code}::${color}`
if (this.secondColorOptionsByKey[key]) {
return this.secondColorOptionsByKey[key]
}
try {
const res = await api.get('/product-secondcolor', { params: { code, color } })
const data = res?.data
const list = Array.isArray(data) ? data : []
this.secondColorOptionsByKey[key] = list
return list
} catch (err) {
this.error = err?.response?.data || err?.message || '2. renk listesi alinamadi'
return []
}
},
async validateUpdates (orderHeaderID, lines) {
if (!orderHeaderID) return { missingCount: 0, missing: [] }
this.saving = true
this.error = null
try {
const res = await api.post(
`/orders/production-items/${encodeURIComponent(orderHeaderID)}/validate`,
{ lines }
)
return res?.data || { missingCount: 0, missing: [] }
} catch (err) {
this.error = err?.response?.data || err?.message || 'Kontrol basarisiz'
throw err
} finally {
this.saving = false
}
},
async applyUpdates (orderHeaderID, lines, insertMissing) {
if (!orderHeaderID) return { updated: 0, inserted: 0 }
this.saving = true
this.error = null
try {
const res = await api.post(
`/orders/production-items/${encodeURIComponent(orderHeaderID)}/apply`,
{ lines, insertMissing }
)
return res?.data || { updated: 0, inserted: 0 }
} catch (err) {
this.error = err?.response?.data || err?.message || 'Guncelleme basarisiz'
throw err
} finally {
this.saving = false
}
}
}
})

View File

@@ -0,0 +1,166 @@
// src/stores/OrderProductionUpdateStore.js
import { defineStore } from 'pinia'
import api from 'src/services/api'
let lastRequestId = 0
export const useOrderProductionUpdateStore = defineStore('orderproductionupdate', {
state: () => ({
orders: [],
loading: false,
error: null,
filters: {
search: '',
CurrAccCode: '',
OrderDate: ''
}
}),
getters: {
filteredOrders (state) {
let result = state.orders
if (state.filters.CurrAccCode) {
result = result.filter(o => o.CurrAccCode === state.filters.CurrAccCode)
}
if (state.filters.OrderDate) {
result = result.filter(o =>
o.OrderDate?.startsWith(state.filters.OrderDate)
)
}
return result
},
totalVisibleUSD () {
return this.filteredOrders.reduce((sum, o) => {
const value = Number(o.TotalAmountUSD || 0)
return sum + (Number.isFinite(value) ? value : 0)
}, 0)
},
totalPackedVisibleUSD () {
return this.filteredOrders.reduce((sum, o) => {
const value = Number(o.PackedUSD || 0)
return sum + (Number.isFinite(value) ? value : 0)
}, 0)
},
packedVisibleRatePct () {
if (!this.totalVisibleUSD) return 0
return (this.totalPackedVisibleUSD / this.totalVisibleUSD) * 100
}
},
actions: {
async fetchOrders () {
// ==============================
// 📌 REQUEST ID
// ==============================
const rid = ++lastRequestId
// ==============================
// 📌 SEARCH SNAPSHOT
// ==============================
const raw = this.filters.search ?? ''
const trimmed = String(raw).trim()
// ==============================
// 📌 REQUEST LOG
// ==============================
console.groupCollapsed(
`%c[orders-prod] FETCH rid=${rid}`,
'color:#1976d2;font-weight:bold'
)
console.log('raw =', JSON.stringify(raw), 'len=', String(raw).length)
console.log('trimmed =', JSON.stringify(trimmed), 'len=', trimmed.length)
console.log('filters =', JSON.parse(JSON.stringify(this.filters)))
console.log('lastRID =', lastRequestId)
console.groupEnd()
this.loading = true
this.error = null
try {
// ==============================
// 📌 PARAMS
// ==============================
const params = {}
if (trimmed) params.search = trimmed
// ==============================
// 📌 API CALL
// ==============================
const res = await api.get('/orders/production-list', { params })
// ==============================
// 📌 STALE CHECK
// ==============================
if (rid !== lastRequestId) {
console.warn(
`[orders-prod] IGNORE stale response rid=${rid} last=${lastRequestId}`
)
return
}
// ==============================
// 📌 DATA
// ==============================
const data = res?.data
this.orders = Array.isArray(data) ? data : []
// ==============================
// 📌 RESPONSE LOG
// ==============================
console.groupCollapsed(
`%c[orders-prod] RESPONSE rid=${rid} count=${this.orders.length}`,
'color:#2e7d32;font-weight:bold'
)
console.log('status =', res?.status)
console.log(
'sample =',
this.orders.slice(0, 5).map(o => ({
id: o.OrderHeaderID,
no: o.OrderNumber,
code: o.CurrAccCode,
name: o.CurrAccDescription
}))
)
console.groupEnd()
} catch (err) {
if (rid !== lastRequestId) return
console.error(
'[orders-prod] FETCH FAILED',
err?.response?.status,
err?.response?.data || err
)
this.orders = []
this.error =
err?.response?.data ||
err?.message ||
'Sipariş listesi alınamadı'
} finally {
if (rid === lastRequestId) {
this.loading = false
}
}
}
}
})

View File

@@ -78,15 +78,7 @@ export const useAuthStore = defineStore('auth', {
========================================================= */ ========================================================= */
setSession ({ token, user }) { setSession ({ token, user }) {
this.token = token this.token = token
if (user) { this.user = user || null
// Keep prior role fields if backend returns partial user payload.
this.user = {
...(this.user || {}),
...user
}
} else {
this.user = null
}
this.forcePasswordChange = !!user?.force_password_change this.forcePasswordChange = !!user?.force_password_change
localStorage.setItem('token', token) localStorage.setItem('token', token)

View File

@@ -43,11 +43,11 @@ export function buildComboKey(row, beden) {
export const BEDEN_SCHEMA = [ export const BEDEN_SCHEMA = [
{ key: 'tak', title: 'TAKIM ELBISE', values: ['44','46','48','50','52','54','56','58','60','62','64','66','68','70','72','74'] },
{ key: 'ayk', title: 'AYAKKABI', values: ['39','40','41','42','43','44','45'] }, { key: 'ayk', title: 'AYAKKABI', values: ['39','40','41','42','43','44','45'] },
{ key: 'yas', title: 'YAS', values: ['2','4','6','8','10','12','14'] }, { key: 'yas', title: 'YAS', values: ['2','4','6','8','10','12','14'] },
{ key: 'pan', title: 'PANTOLON', values: ['38','40','42','44','46','48','50','52','54','56','58','60','62','64','66','68'] }, { key: 'pan', title: 'PANTOLON', values: ['38','40','42','44','46','48','50','52','54','56','58','60','62','64','66','68'] },
{ key: 'gom', title: 'GOMLEK', values: ['XS','S','M','L','XL','2XL','3XL','4XL','5XL','6XL','7XL'] }, { key: 'gom', title: 'GOMLEK', values: ['XS','S','M','L','XL','2XL','3XL','4XL','5XL','6XL','7XL'] },
{ key: 'tak', title: 'TAKIM ELBISE', values: ['44','46','48','50','52','54','56','58','60','62','64','66','68','70','72','74'] },
{ key: 'aksbir', title: 'AKSESUAR', values: [' ', '44', 'STD', '110', '115', '120', '125', '130', '135'] } { key: 'aksbir', title: 'AKSESUAR', values: [' ', '44', 'STD', '110', '115', '120', '125', '130', '135'] }
] ]
@@ -1705,6 +1705,7 @@ export const useOrderEntryStore = defineStore('orderentry', {
rows[idx] = { rows[idx] = {
...prev, ...prev,
...newRow, ...newRow,
_dirty: true,
id: prev.id, id: prev.id,
OrderLineID: prev.OrderLineID || null, OrderLineID: prev.OrderLineID || null,
lineIdMap: preservedLineIdMap lineIdMap: preservedLineIdMap
@@ -1763,6 +1764,7 @@ export const useOrderEntryStore = defineStore('orderentry', {
const insertedRow = { const insertedRow = {
...newRow, ...newRow,
_dirty: true,
id: crypto.randomUUID(), id: crypto.randomUUID(),
OrderLineID: null, OrderLineID: null,
lineIdMap: {} lineIdMap: {}
@@ -1838,6 +1840,7 @@ export const useOrderEntryStore = defineStore('orderentry', {
rows[dupIdx] = { rows[dupIdx] = {
...prev, ...prev,
...newRow, ...newRow,
_dirty: true,
// kritik korumalar // kritik korumalar
id: prev.id, id: prev.id,
@@ -1871,6 +1874,7 @@ export const useOrderEntryStore = defineStore('orderentry', {
// dup yoksa (veya dup delete satırıydı) → yeni satır // dup yoksa (veya dup delete satırıydı) → yeni satır
rows.push({ rows.push({
...newRow, ...newRow,
_dirty: true,
id: newRow.id || crypto.randomUUID(), id: newRow.id || crypto.randomUUID(),
OrderLineID: null, OrderLineID: null,
lineIdMap: { ...(newRow.lineIdMap || {}) } lineIdMap: { ...(newRow.lineIdMap || {}) }
@@ -2529,6 +2533,50 @@ export const useOrderEntryStore = defineStore('orderentry', {
console.log(`🧭 Order mode set edildi → ${mode}`) console.log(`🧭 Order mode set edildi → ${mode}`)
} }
, ,
// Sync only header-related fields from the form before submit.
syncHeaderFromForm(form) {
if (!form || typeof form !== 'object') return
const keys = [
'OrderHeaderID',
'OrderTypeCode',
'ProcessCode',
'OrderNumber',
'OrderDate',
'AverageDueDate',
'Description',
'InternalDescription',
'CurrAccTypeCode',
'CurrAccCode',
'CurrAccDescription',
'DocCurrencyCode',
'LocalCurrencyCode',
'ExchangeRate',
'OfficeCode',
'CreatedUserName',
'CreatedDate',
'LastUpdatedUserName',
'LastUpdatedDate',
'PaymentTerm',
'WarehouseCode',
'StoreCode'
]
const patch = {}
for (const k of keys) {
if (Object.prototype.hasOwnProperty.call(form, k)) {
patch[k] = form[k]
}
}
if (Object.keys(patch).length > 0) {
this.header = {
...(this.header || {}),
...patch
}
}
}
,
/* =========================================================== /* ===========================================================
🟦 submitAllReal (v12.1c — FINAL / CLEAN + PRE-VALIDATE) 🟦 submitAllReal (v12.1c — FINAL / CLEAN + PRE-VALIDATE)
----------------------------------------------------------- -----------------------------------------------------------
@@ -2549,6 +2597,9 @@ export const useOrderEntryStore = defineStore('orderentry', {
// 🔒 Kontrollü submit → route leave guard susar // 🔒 Kontrollü submit → route leave guard susar
this.isControlledSubmit = true this.isControlledSubmit = true
// ✅ Formdaki header alanlarını store'a taşı
this.syncHeaderFromForm?.(form)
const isNew = this.mode === 'new' const isNew = this.mode === 'new'
const { header, lines } = this.buildFinalOrderJson() const { header, lines } = this.buildFinalOrderJson()
@@ -2633,12 +2684,18 @@ export const useOrderEntryStore = defineStore('orderentry', {
// 🧪 PRE-VALIDATE — prItemVariant ön kontrol // 🧪 PRE-VALIDATE — prItemVariant ön kontrol
// - invalid varsa CREATE/UPDATE ÇALIŞMAZ // - invalid varsa CREATE/UPDATE ÇALIŞMAZ
// ======================================================= // =======================================================
const v = await api.post('/order/validate', { header, lines }) if (!isNew) {
const linesToValidate = lines.filter(
l => l._deleteSignal === true || l._dirty === true || !l.OrderLineID
)
const v = await api.post('/order/validate', { header, lines: linesToValidate })
const invalid = v?.data?.invalid || [] const invalid = v?.data?.invalid || []
if (invalid.length > 0) { if (invalid.length > 0) {
await this.showInvalidVariantDialog?.($q, invalid) await this.showInvalidVariantDialog?.($q, invalid)
return // ❌ create / update ÇALIŞMAZ return // ❌ update ÇALIŞMAZ
}
} }
console.log('📤 submitAllReal payload', { console.log('📤 submitAllReal payload', {
@@ -2973,6 +3030,8 @@ export const useOrderEntryStore = defineStore('orderentry', {
OrderLineID: orderLineId || '', OrderLineID: orderLineId || '',
ClientKey: makeLineClientKey(row, grpKey, bedenKey), ClientKey: makeLineClientKey(row, grpKey, bedenKey),
ComboKey: comboKey, ComboKey: comboKey,
_dirty: row?._dirty === true,
_deleteSignal: isDeleteSignal === true,
SortOrder: 0, SortOrder: 0,
ItemTypeCode: 1, ItemTypeCode: 1,
@@ -3054,12 +3113,16 @@ export const useOrderEntryStore = defineStore('orderentry', {
if (orderLineId && !existing.OrderLineID) { if (orderLineId && !existing.OrderLineID) {
existing.OrderLineID = orderLineId existing.OrderLineID = orderLineId
} }
existing._deleteSignal = true
existing.Qty1 = 0 existing.Qty1 = 0
return return
} }
/* MERGE */ /* MERGE */
existing.Qty1 += qty existing.Qty1 += qty
if (row?._dirty === true) {
existing._dirty = true
}
if (this.mode === 'edit' && orderLineId && !existing.OrderLineID) { if (this.mode === 'edit' && orderLineId && !existing.OrderLineID) {
existing.OrderLineID = orderLineId existing.OrderLineID = orderLineId
@@ -3346,66 +3409,56 @@ export function normalizeBeden(v) {
- Keeps frontend aksbir bucket for accessory lines - Keeps frontend aksbir bucket for accessory lines
=========================================================== */ =========================================================== */
export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '') { export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '') {
const list = Array.isArray(bedenList) ? bedenList : [] const list = Array.isArray(bedenList) && bedenList.length > 0
const ana = normalizeTextForMatch(urunAnaGrubu) ? bedenList.map(v => (v || '').toString().trim().toUpperCase())
const alt = normalizeTextForMatch(urunKategori) : [' ']
// Frontend compatibility: accessory-only products should stay in aksbir. const ana = (urunAnaGrubu || '')
const accessoryGroups = [ .toUpperCase()
'AKSESUAR', 'KRAVAT', 'PAPYON', 'KEMER', 'CORAP', .trim()
'FULAR', 'MENDIL', 'KASKOL', 'ASKI', 'YAKA', 'KOL DUGMESI' .replace(/\(.*?\)/g, '')
.replace(/[^A-ZÇĞİÖŞÜ0-9\s]/g, '')
.replace(/\s+/g, ' ')
const kat = (urunKategori || '').toUpperCase().trim()
// 🔸 Aksesuar ise "aksbir"
const aksesuarGruplari = [
'AKSESUAR','KRAVAT','PAPYON','KEMER','CORAP','ÇORAP',
'FULAR','MENDIL','MENDİL','KASKOL','ASKI',
'YAKA','KOL DUGMESI','KOL DÜĞMESİ'
] ]
const clothingGroups = ['GOMLEK', 'CEKET', 'PANTOLON', 'MONT', 'YELEK', 'TAKIM', 'TSHIRT'] const giyimGruplari = ['GÖMLEK','CEKET','PANTOLON','MONT','YELEK','TAKIM','TSHIRT','TİŞÖRT']
// 🔸 Pantolon özel durumu
if ( if (
accessoryGroups.some(g => ana.includes(g) || alt.includes(g)) && aksesuarGruplari.some(g => ana.includes(g) || kat.includes(g)) &&
!clothingGroups.some(g => ana.includes(g)) !giyimGruplari.some(g => ana.includes(g))
) { ) return 'aksbir'
return 'aksbir'
if (ana.includes('PANTOLON') && kat.includes('YETİŞKİN')) return 'pan'
// 🔸 Tamamen numerik (örneğin 39-44 arası) → ayakkabı
const allNumeric = list.every(v => /^\d+$/.test(v))
if (allNumeric) {
const nums = list.map(v => parseInt(v, 10)).filter(Boolean)
const diffs = nums.slice(1).map((v, i) => v - nums[i])
if (diffs.every(d => d === 1) && nums[0] >= 35 && nums[0] <= 46) return 'ayk'
} }
if (ana.includes('AYAKKABI') || alt.includes('AYAKKABI')) { // 🔸 Yaş grubu (çocuk/garson)
return 'ayk' if (kat.includes('GARSON') || kat.includes('ÇOCUK')) return 'yas'
}
let hasYasNumeric = false // 🔸 Harfli beden varsa doğrudan "gom" (gömlek, üst giyim)
let hasAykNumeric = false const harfliBedenler = ['XS','S','M','L','XL','XXL','3XL','4XL']
let hasPanNumeric = false if (list.some(b => harfliBedenler.includes(b))) return 'gom'
for (const raw of list) {
const b = safeTrimUpperJs(raw)
switch (b) {
case 'XS':
case 'S':
case 'M':
case 'L':
case 'XL':
case '2XL':
case '3XL':
case '4XL':
case '5XL':
case '6XL':
case '7XL':
return 'gom'
}
const n = parseNumericSizeJs(b)
if (n == null) continue
if (n >= 2 && n <= 14) hasYasNumeric = true
if (n >= 39 && n <= 45) hasAykNumeric = true
if (n >= 38 && n <= 68) hasPanNumeric = true
}
if (hasAykNumeric) return 'ayk'
if (ana.includes('PANTOLON')) return 'pan'
if (hasPanNumeric) return 'pan'
if (alt.includes('COCUK') || alt.includes('GARSON')) return 'yas'
if (hasYasNumeric) return 'yas'
// 🔸 Varsayılan: takım elbise
return 'tak' return 'tak'
} }
export function toSummaryRowFromForm(form) { export function toSummaryRowFromForm(form) {
if (!form) return null if (!form) return null

View File

@@ -165,13 +165,12 @@ export const usePermissionStore = defineStore('permission', {
try { try {
// API ROUTES const [routesRes, effRes] = await Promise.all([
const routesRes = await api.get('/permissions/routes') api.get('/permissions/routes'),
api.get('/permissions/effective')
])
this.routes = routesRes.data || [] this.routes = routesRes.data || []
// EFFECTIVE MATRIX
const effRes = await api.get('/permissions/effective')
this.matrix = effRes.data || [] this.matrix = effRes.data || []
console.group('🔐 PERMISSION DEBUG') console.group('🔐 PERMISSION DEBUG')