Compare commits
112 Commits
76fa2040b1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ced1b1649 | ||
|
|
76e7ca2e4a | ||
|
|
ed81fdf84f | ||
|
|
026c40c0b3 | ||
|
|
0136e6638b | ||
|
|
7184a40dd3 | ||
|
|
de58ef1043 | ||
|
|
744e20591d | ||
|
|
1263531edd | ||
|
|
d2bd0684c1 | ||
|
|
13f8801379 | ||
|
|
c3a1627152 | ||
|
|
727069910d | ||
|
|
1f95099677 | ||
|
|
dc36699a2b | ||
|
|
0e63370810 | ||
|
|
ea7d426436 | ||
|
|
369db87091 | ||
|
|
400220995b | ||
|
|
eff80a3211 | ||
|
|
00fc69601e | ||
|
|
3d508868c8 | ||
|
|
46c617b8f5 | ||
|
|
3bbb8539c7 | ||
|
|
5ca00065e6 | ||
|
|
93446e6a69 | ||
|
|
4b455814b4 | ||
|
|
291603163b | ||
|
|
e4cdae58d4 | ||
|
|
6483678267 | ||
|
|
fde9b4469f | ||
|
|
90ed98d59f | ||
|
|
88c20d844f | ||
|
|
cf8352dbaf | ||
|
|
5429305a6e | ||
|
|
88c189a48d | ||
|
|
8eaee91537 | ||
|
|
199390bc66 | ||
|
|
1c1df2521e | ||
|
|
68790c9f4e | ||
|
|
3eac743225 | ||
|
|
d82cea0b54 | ||
|
|
8c0f18eee3 | ||
|
|
3faaf57768 | ||
|
|
10adf327f4 | ||
|
|
47b3e9172f | ||
|
|
9cf575d71d | ||
|
|
585e98afb8 | ||
|
|
76ba649366 | ||
|
|
801ed23cd9 | ||
|
|
2c6515b580 | ||
|
|
6f842043b2 | ||
|
|
7edf87055e | ||
|
|
484512ff25 | ||
|
|
0a14f87a3e | ||
|
|
daedff2880 | ||
|
|
54182e97c5 | ||
|
|
14d71ba925 | ||
|
|
5124ad78af | ||
|
|
f8b07a6aea | ||
|
|
f5f37089ac | ||
|
|
4b01a0835d | ||
|
|
82e51bbfcd | ||
|
|
0a5ffe1407 | ||
|
|
5a6350250a | ||
|
|
9ce85ff6b8 | ||
|
|
f6e1e7d00e | ||
|
|
a514f4dcfa | ||
|
|
2f9c917a08 | ||
|
|
cb415a6f63 | ||
|
|
a1ab7508c6 | ||
|
|
ec6d547641 | ||
|
|
96b973b71f | ||
|
|
5c5916f58c | ||
|
|
f767726617 | ||
|
|
fcd31c4d7f | ||
|
|
756bbe137d | ||
|
|
cd8c8a6e9e | ||
|
|
70f097806b | ||
|
|
21db754045 | ||
|
|
56e40de192 | ||
|
|
f338ef1986 | ||
|
|
e7a776aede | ||
|
|
897b153cfc | ||
|
|
babf77ae17 | ||
|
|
43d018f492 | ||
|
|
434908495e | ||
|
|
a236686f7d | ||
|
|
400fa38a49 | ||
|
|
4be05573eb | ||
|
|
9e18ac1398 | ||
|
|
7d94573bdd | ||
|
|
a2a756870d | ||
|
|
d0f20674ea | ||
|
|
237f73a923 | ||
|
|
ce110ed86f | ||
|
|
6d22f5874a | ||
|
|
6105be3eb3 | ||
|
|
fd5b8a2954 | ||
|
|
14b3d86782 | ||
|
|
f079ee80da | ||
|
|
3bc3543010 | ||
|
|
4355a09a15 | ||
|
|
5eca170e97 | ||
|
|
676724b5d5 | ||
|
|
1874e4c0d3 | ||
|
|
a3456d388d | ||
|
|
e38421db45 | ||
|
|
c6007c05cb | ||
|
|
406d9e8ce5 | ||
|
|
1d15b619f9 | ||
|
|
7b0850eb4e |
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Deploy output
|
||||||
|
svc/public/
|
||||||
|
svc/public/**
|
||||||
|
|
||||||
|
ui/dist/
|
||||||
|
ui/dist/**
|
||||||
212
deploy/deploy.sh
Normal file
212
deploy/deploy.sh
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
umask 022
|
||||||
|
|
||||||
|
export NODE_OPTIONS="--max_old_space_size=4096"
|
||||||
|
export CI="true"
|
||||||
|
export npm_config_progress="false"
|
||||||
|
export npm_config_loglevel="warn"
|
||||||
|
export FORCE_COLOR="0"
|
||||||
|
|
||||||
|
LOG_FILE="/var/log/bssapp_deploy.log"
|
||||||
|
APP_DIR="/opt/bssapp"
|
||||||
|
LOCK_FILE="/tmp/bssapp_deploy.lock"
|
||||||
|
RUNTIME_BACKUP_DIR=""
|
||||||
|
RUNTIME_PRESERVE_FILES=(
|
||||||
|
".env"
|
||||||
|
"mail.env"
|
||||||
|
"svc/.env"
|
||||||
|
"svc/mail.env"
|
||||||
|
"svc/fonts"
|
||||||
|
"svc/public"
|
||||||
|
)
|
||||||
|
|
||||||
|
log_step() {
|
||||||
|
echo "== $1 =="
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_runtime_files() {
|
||||||
|
RUNTIME_BACKUP_DIR="$(mktemp -d /tmp/bssapp-runtime.XXXXXX)"
|
||||||
|
|
||||||
|
for rel in "${RUNTIME_PRESERVE_FILES[@]}"; do
|
||||||
|
src="$APP_DIR/$rel"
|
||||||
|
dst="$RUNTIME_BACKUP_DIR/$rel"
|
||||||
|
if [[ -e "$src" ]]; then
|
||||||
|
mkdir -p "$(dirname "$dst")"
|
||||||
|
cp -a "$src" "$dst"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_runtime_files() {
|
||||||
|
[[ -n "$RUNTIME_BACKUP_DIR" && -d "$RUNTIME_BACKUP_DIR" ]] || return 0
|
||||||
|
find "$RUNTIME_BACKUP_DIR" -mindepth 1 -print -quit | grep -q . || return 0
|
||||||
|
cp -a "$RUNTIME_BACKUP_DIR/." "$APP_DIR/"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_runtime_backup() {
|
||||||
|
if [[ -n "$RUNTIME_BACKUP_DIR" && -d "$RUNTIME_BACKUP_DIR" ]]; then
|
||||||
|
rm -rf "$RUNTIME_BACKUP_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_runtime_env_files() {
|
||||||
|
[[ -f "$APP_DIR/.env" ]] || touch "$APP_DIR/.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/mail.env" ]] || touch "$APP_DIR/svc/mail.env"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_pdf_fonts() {
|
||||||
|
local font_dir="$APP_DIR/svc/fonts"
|
||||||
|
local sys_font_dir="/usr/share/fonts/truetype/dejavu"
|
||||||
|
|
||||||
|
mkdir -p "$font_dir"
|
||||||
|
|
||||||
|
if [[ ! -f "$font_dir/DejaVuSans.ttf" && -f "$sys_font_dir/DejaVuSans.ttf" ]]; then
|
||||||
|
cp -a "$sys_font_dir/DejaVuSans.ttf" "$font_dir/DejaVuSans.ttf"
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$font_dir/DejaVuSans-Bold.ttf" && -f "$sys_font_dir/DejaVuSans-Bold.ttf" ]]; then
|
||||||
|
cp -a "$sys_font_dir/DejaVuSans-Bold.ttf" "$font_dir/DejaVuSans-Bold.ttf"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$font_dir/DejaVuSans.ttf" || ! -f "$font_dir/DejaVuSans-Bold.ttf" ]]; then
|
||||||
|
echo "ERROR: Required PDF fonts missing in $font_dir"
|
||||||
|
return 1
|
||||||
|
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() {
|
||||||
|
if ! command -v go >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: go command not found"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export GOPATH="${GOPATH:-/var/cache/bssapp-go}"
|
||||||
|
export GOMODCACHE="${GOMODCACHE:-$GOPATH/pkg/mod}"
|
||||||
|
export GOCACHE="${GOCACHE:-/var/cache/bssapp-go-build}"
|
||||||
|
mkdir -p "$GOPATH" "$GOMODCACHE" "$GOCACHE"
|
||||||
|
|
||||||
|
cd "$APP_DIR/svc"
|
||||||
|
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-w -s" -o "$APP_DIR/svc/bssapp" ./main.go
|
||||||
|
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() {
|
||||||
|
trap cleanup_runtime_backup EXIT
|
||||||
|
|
||||||
|
exec 9>"$LOCK_FILE"
|
||||||
|
if ! flock -n 9; then
|
||||||
|
echo "[$(date '+%F %T')] Deploy already running. Skipping new request."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=============================="
|
||||||
|
echo "[DEPLOY START] $(date '+%F %T')"
|
||||||
|
echo "=============================="
|
||||||
|
|
||||||
|
cd "$APP_DIR"
|
||||||
|
|
||||||
|
log_step "GIT SYNC"
|
||||||
|
backup_runtime_files
|
||||||
|
git fetch origin
|
||||||
|
git reset --hard origin/master
|
||||||
|
git clean -fdx \
|
||||||
|
-e .env \
|
||||||
|
-e mail.env \
|
||||||
|
-e svc/.env \
|
||||||
|
-e svc/mail.env \
|
||||||
|
-e svc/fonts \
|
||||||
|
-e svc/public \
|
||||||
|
-e svc/bssapp
|
||||||
|
restore_runtime_files
|
||||||
|
echo "DEPLOY COMMIT: $(git rev-parse --short HEAD)"
|
||||||
|
|
||||||
|
log_step "BUILD UI"
|
||||||
|
cd "$APP_DIR/ui"
|
||||||
|
npm ci --no-audit --no-fund --include=optional
|
||||||
|
npm i -D --no-audit --no-fund sass-embedded@1.93.2
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
log_step "ENSURE UI PERMISSIONS"
|
||||||
|
ensure_ui_permissions
|
||||||
|
ensure_ui_readable_by_nginx
|
||||||
|
|
||||||
|
log_step "BUILD API"
|
||||||
|
build_api_binary
|
||||||
|
|
||||||
|
log_step "ENSURE ENV FILES"
|
||||||
|
ensure_runtime_env_files
|
||||||
|
|
||||||
|
log_step "ENSURE PDF FONTS"
|
||||||
|
ensure_pdf_fonts
|
||||||
|
|
||||||
|
log_step "RESTART SERVICES"
|
||||||
|
restart_services
|
||||||
|
|
||||||
|
echo "[DEPLOY FINISHED] $(date '+%F %T')"
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
nohup /bin/bash "$0" --run </dev/null >/dev/null 2>&1 &
|
||||||
|
exit 0
|
||||||
57
deploy/hooks.json
Normal file
57
deploy/hooks.json
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "bssapp-deploy",
|
||||||
|
"execute-command": "/bin/bash",
|
||||||
|
"command-working-directory": "/opt/bssapp",
|
||||||
|
"pass-arguments-to-command": [
|
||||||
|
{
|
||||||
|
"source": "string",
|
||||||
|
"name": "/opt/bssapp/deploy/deploy.sh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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": {
|
||||||
|
"type": "value",
|
||||||
|
"value": "bssapp-secret-2026",
|
||||||
|
"parameter": {
|
||||||
|
"source": "header",
|
||||||
|
"name": "X-BSSAPP-SECRET"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
10
svc/.env
10
svc/.env
@@ -1,10 +0,0 @@
|
|||||||
JWT_SECRET=bssapp_super_secret_key_1234567890
|
|
||||||
PASSWORD_RESET_SECRET=1dc7d6d52fd0459a8b1f288a6590428e760f54339f8e47beb20db36b6df6070b
|
|
||||||
APP_FRONTEND_URL=http://localhost:9000
|
|
||||||
API_URL=http://localhost:8080
|
|
||||||
UI_DIR=/opt/bssapp/ui/dist
|
|
||||||
POSTGRES_CONN=host=127.0.0.1 port=5432 user=postgres password=tayitkan dbname=baggib2b sslmode=disable
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
33
svc/.env.local
Normal file
33
svc/.env.local
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# ===============================
|
||||||
|
# SECURITY
|
||||||
|
# ===============================
|
||||||
|
JWT_SECRET=bssapp_super_secret_key_1234567890
|
||||||
|
PASSWORD_RESET_SECRET=1dc7d6d52fd0459a8b1f288a6590428e760f54339f8e47beb20db36b6df6070b
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# URLS (PRODUCTION)
|
||||||
|
# ===============================
|
||||||
|
APP_FRONTEND_URL=http://46.224.33.150
|
||||||
|
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
|
||||||
|
|
||||||
13
svc/.gitignore
vendored
Normal file
13
svc/.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Binary
|
||||||
|
bssapp
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
mail.env
|
||||||
|
|
||||||
|
# Runtime fonts
|
||||||
|
fonts/*.ttf
|
||||||
|
|
||||||
|
# Go
|
||||||
|
.gocache
|
||||||
|
*.exe
|
||||||
@@ -4,15 +4,20 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
_ "github.com/microsoft/go-mssqldb"
|
_ "github.com/microsoft/go-mssqldb"
|
||||||
)
|
)
|
||||||
|
|
||||||
var MssqlDB *sql.DB
|
var MssqlDB *sql.DB
|
||||||
|
|
||||||
|
// ConnectMSSQL MSSQL baglantisini ortam degiskeninden baslatir.
|
||||||
func ConnectMSSQL() {
|
func ConnectMSSQL() {
|
||||||
//connString := "sqlserver://sa:Gil_0150@10.0.0.9:1433?databaseName=BAGGI_V3"
|
connString := strings.TrimSpace(os.Getenv("MSSQL_CONN"))
|
||||||
connString := "sqlserver://sa:Gil_0150@100.127.221.13:1433?databaseName=BAGGI_V3"
|
if connString == "" {
|
||||||
|
log.Fatal("MSSQL_CONN tanımlı değil")
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
MssqlDB, err = sql.Open("sqlserver", connString)
|
MssqlDB, err = sql.Open("sqlserver", connString)
|
||||||
@@ -20,13 +25,13 @@ 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 {
|
||||||
return MssqlDB
|
return MssqlDB
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 .env’den 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()
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1,4 +0,0 @@
|
|||||||
AZURE_TENANT_ID=c8e0675d-1f6e-40f3-ba5f-3d1985b92317
|
|
||||||
AZURE_CLIENT_ID=94a134b7-757f-4bcc-9e4b-d577b631a9a3
|
|
||||||
AZURE_CLIENT_SECRET=PaW8Q~9NzYXHrESZcKoP6.hRxS.CyQshvJ0Y0czx
|
|
||||||
MAIL_FROM=baggiss@baggi.com.tr
|
|
||||||
51
svc/main.go
51
svc/main.go
@@ -212,19 +212,19 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
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 +235,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 +248,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 +286,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)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -325,6 +325,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
"user", "update",
|
"user", "update",
|
||||||
wrapV3(routes.UserDetailRoute(pgDB)),
|
wrapV3(routes.UserDetailRoute(pgDB)),
|
||||||
)
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/users/{id}", "DELETE",
|
||||||
|
"user", "delete",
|
||||||
|
wrapV3(routes.UserDetailRoute(pgDB)),
|
||||||
|
)
|
||||||
|
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/users/{id}/admin-reset-password", "POST",
|
"/api/users/{id}/admin-reset-password", "POST",
|
||||||
@@ -611,8 +616,21 @@ func main() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
log.Println("✅ Server çalışıyor: http://localhost:8080")
|
host := strings.TrimSpace(os.Getenv("API_HOST"))
|
||||||
log.Fatal(http.ListenAndServe(":8080", handler))
|
port := strings.TrimSpace(os.Getenv("API_PORT"))
|
||||||
|
|
||||||
|
if host == "" {
|
||||||
|
host = "0.0.0.0"
|
||||||
|
}
|
||||||
|
if port == "" {
|
||||||
|
port = "8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := host + ":" + port
|
||||||
|
|
||||||
|
log.Println("🚀 Server running at:", addr)
|
||||||
|
log.Fatal(http.ListenAndServe(addr, handler))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func mountSPA(r *mux.Router) {
|
func mountSPA(r *mux.Router) {
|
||||||
@@ -672,12 +690,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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,29 +10,49 @@ import (
|
|||||||
|
|
||||||
func AuthMiddleware(db *sql.DB, next http.Handler) http.Handler {
|
func AuthMiddleware(db *sql.DB, next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
authHeader := r.Header.Get("Authorization")
|
authHeader := r.Header.Get("Authorization")
|
||||||
if authHeader == "" {
|
if authHeader == "" {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
log.Printf(
|
||||||
|
"AUTH_MIDDLEWARE 401 reason=missing_authorization_header method=%s path=%s",
|
||||||
|
r.Method,
|
||||||
|
r.URL.Path,
|
||||||
|
)
|
||||||
|
http.Error(w, "unauthorized: authorization header missing", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.SplitN(authHeader, " ", 2)
|
parts := strings.SplitN(authHeader, " ", 2)
|
||||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
log.Printf(
|
||||||
|
"AUTH_MIDDLEWARE 401 reason=invalid_authorization_format method=%s path=%s raw=%q",
|
||||||
|
r.Method,
|
||||||
|
r.URL.Path,
|
||||||
|
authHeader,
|
||||||
|
)
|
||||||
|
http.Error(w, "unauthorized: invalid authorization format", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
claims, err := auth.ValidateToken(parts[1])
|
claims, err := auth.ValidateToken(parts[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
log.Printf(
|
||||||
|
"AUTH_MIDDLEWARE 401 reason=token_validation_failed method=%s path=%s err=%v",
|
||||||
|
r.Method,
|
||||||
|
r.URL.Path,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
http.Error(w, "unauthorized: token validation failed", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 BU SATIR ŞART
|
|
||||||
ctx := auth.WithClaims(r.Context(), claims)
|
ctx := auth.WithClaims(r.Context(), claims)
|
||||||
|
log.Printf(
|
||||||
log.Printf("🔐 AUTH CTX SET user=%d role=%s", claims.ID, claims.RoleCode)
|
"AUTH_MIDDLEWARE PASS user=%d role=%s method=%s path=%s",
|
||||||
|
claims.ID,
|
||||||
|
claims.RoleCode,
|
||||||
|
r.Method,
|
||||||
|
r.URL.Path,
|
||||||
|
)
|
||||||
|
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -859,7 +859,12 @@ func AuthzGuardByRoute(pg *sql.DB) func(http.Handler) http.Handler {
|
|||||||
// =====================================================
|
// =====================================================
|
||||||
claims, ok := auth.GetClaimsFromContext(r.Context())
|
claims, ok := auth.GetClaimsFromContext(r.Context())
|
||||||
if !ok || claims == nil {
|
if !ok || claims == nil {
|
||||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
log.Printf(
|
||||||
|
"AUTHZ_BY_ROUTE 401 reason=claims_missing method=%s path=%s",
|
||||||
|
r.Method,
|
||||||
|
r.URL.Path,
|
||||||
|
)
|
||||||
|
http.Error(w, "unauthorized: token missing or invalid", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,7 +878,7 @@ func AuthzGuardByRoute(pg *sql.DB) func(http.Handler) http.Handler {
|
|||||||
r.Method, r.URL.Path,
|
r.Method, r.URL.Path,
|
||||||
)
|
)
|
||||||
|
|
||||||
http.Error(w, "route not resolved", 403)
|
http.Error(w, "route not resolved", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -881,7 +886,22 @@ func AuthzGuardByRoute(pg *sql.DB) func(http.Handler) http.Handler {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("❌ AUTHZ: path template error: %v", err)
|
log.Printf("❌ AUTHZ: path template error: %v", err)
|
||||||
|
|
||||||
http.Error(w, "route template error", 403)
|
http.Error(w, "route template error", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password change must be reachable for every authenticated user.
|
||||||
|
// This avoids permission deadlocks during forced first-password flow.
|
||||||
|
if pathTemplate == "/api/password/change" {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Self permission endpoints are required right after login
|
||||||
|
// to hydrate UI permission state for the authenticated user.
|
||||||
|
switch pathTemplate {
|
||||||
|
case "/api/permissions/routes", "/api/permissions/effective":
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -908,7 +928,12 @@ func AuthzGuardByRoute(pg *sql.DB) func(http.Handler) http.Handler {
|
|||||||
pathTemplate,
|
pathTemplate,
|
||||||
)
|
)
|
||||||
|
|
||||||
http.Error(w, "route permission not found", 403)
|
if pathTemplate == "/api/password/change" {
|
||||||
|
http.Error(w, "password change route permission not found", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Error(w, "route permission not found", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -935,7 +960,12 @@ func AuthzGuardByRoute(pg *sql.DB) func(http.Handler) http.Handler {
|
|||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
|
|
||||||
http.Error(w, "forbidden", 403)
|
if pathTemplate == "/api/password/change" {
|
||||||
|
http.Error(w, "password change permission check failed", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Error(w, "forbidden", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -948,7 +978,12 @@ func AuthzGuardByRoute(pg *sql.DB) func(http.Handler) http.Handler {
|
|||||||
action,
|
action,
|
||||||
)
|
)
|
||||||
|
|
||||||
http.Error(w, "forbidden", 403)
|
if pathTemplate == "/api/password/change" {
|
||||||
|
http.Error(w, "password change forbidden: permission denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Error(w, "forbidden", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 58 KiB |
@@ -183,7 +183,6 @@ WHERE
|
|||||||
AND h.ProcessCode = 'WS'
|
AND h.ProcessCode = 'WS'
|
||||||
AND h.IsClosed = 0
|
AND h.IsClosed = 0
|
||||||
AND ISNULL(l.TotalAmount,0) > 0
|
AND ISNULL(l.TotalAmount,0) > 0
|
||||||
AND (ISNULL(l.PackedAmount,0) * 100.0) / NULLIF(l.TotalAmount,0) >= 100
|
|
||||||
|
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
@@ -322,7 +321,6 @@ WHERE
|
|||||||
) t
|
) t
|
||||||
WHERE t.OrderHeaderID = h.OrderHeaderID
|
WHERE t.OrderHeaderID = h.OrderHeaderID
|
||||||
AND t.TotalAmount > 0
|
AND t.TotalAmount > 0
|
||||||
AND (t.PackedAmount * 100.0) / NULLIF(t.TotalAmount,0) >= 100
|
|
||||||
);
|
);
|
||||||
`, strings.Join(placeholders, ","), piyasaWhere)
|
`, strings.Join(placeholders, ","), piyasaWhere)
|
||||||
|
|
||||||
|
|||||||
@@ -1103,6 +1103,38 @@ func UpdateOrder(header models.OrderHeader, lines []models.OrderDetail, user *mo
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
v3User := buildV3AuditUser(user)
|
v3User := buildV3AuditUser(user)
|
||||||
|
|
||||||
|
// Hard delete bazen FK nedeniyle patlayabiliyor.
|
||||||
|
// Bu durumda satırı soft-close ederek akışı güvenli şekilde tamamlarız.
|
||||||
|
deleteOrSoftCloseLine := func(lineID string) error {
|
||||||
|
if _, err := tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLineCurrency WHERE OrderLineID=@p1`, lineID); err != nil {
|
||||||
|
return fmt.Errorf("line currency delete failed line_id=%s: %w", lineID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := tx.Exec(`
|
||||||
|
DELETE FROM BAGGI_V3.dbo.trOrderLine
|
||||||
|
WHERE OrderHeaderID=@p1 AND OrderLineID=@p2 AND ISNULL(IsClosed,0)=0
|
||||||
|
`, header.OrderHeaderID, lineID); err != nil {
|
||||||
|
fmt.Printf("[ORDER_UPDATE] hard delete failed, trying soft-close line_id=%s err=%v\n", lineID, err)
|
||||||
|
|
||||||
|
if _, err2 := tx.Exec(`
|
||||||
|
UPDATE BAGGI_V3.dbo.trOrderLine
|
||||||
|
SET
|
||||||
|
Qty1 = 0,
|
||||||
|
Qty2 = 0,
|
||||||
|
CancelQty1 = 0,
|
||||||
|
CancelQty2 = 0,
|
||||||
|
IsClosed = 1,
|
||||||
|
LastUpdatedUserName = @p1,
|
||||||
|
LastUpdatedDate = @p2
|
||||||
|
WHERE OrderHeaderID=@p3 AND OrderLineID=@p4 AND ISNULL(IsClosed,0)=0
|
||||||
|
`, 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 nil
|
||||||
|
}
|
||||||
|
|
||||||
// Döviz kuru (Header ExchangeRate fallback)
|
// Döviz kuru (Header ExchangeRate fallback)
|
||||||
exRate := 1.0
|
exRate := 1.0
|
||||||
if header.DocCurrencyCode.Valid && header.DocCurrencyCode.String != "TRY" {
|
if header.DocCurrencyCode.Valid && header.DocCurrencyCode.String != "TRY" {
|
||||||
@@ -1329,13 +1361,7 @@ WHERE OrderLineID=@p42 AND ISNULL(IsClosed,0)=0`)
|
|||||||
Dim2: strings.TrimSpace(safeNS(ln.ItemDim2Code)),
|
Dim2: strings.TrimSpace(safeNS(ln.ItemDim2Code)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, err := tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLineCurrency WHERE OrderLineID=@p1`, ln.OrderLineID); err != nil {
|
if err := deleteOrSoftCloseLine(ln.OrderLineID); err != nil {
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, err := tx.Exec(`
|
|
||||||
DELETE FROM BAGGI_V3.dbo.trOrderLine
|
|
||||||
WHERE OrderHeaderID=@p1 AND OrderLineID=@p2 AND ISNULL(IsClosed,0)=0
|
|
||||||
`, header.OrderHeaderID, ln.OrderLineID); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1356,6 +1382,15 @@ WHERE OrderHeaderID=@p1 AND OrderLineID=@p2 AND ISNULL(IsClosed,0)=0
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Edit akışında bazen stale OrderLineID gelebiliyor (silinmiş/eski satır ID'si).
|
||||||
|
// Böyle bir ID ile UPDATE denemek yerine yeni satır olarak ele al.
|
||||||
|
if !isNew && ln.OrderLineID != "" {
|
||||||
|
if _, stillOpen := existingOpen[ln.OrderLineID]; !stillOpen {
|
||||||
|
ln.OrderLineID = uuid.New().String()
|
||||||
|
isNew = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
@@ -1482,10 +1517,7 @@ WHERE OrderHeaderID=@p1 AND OrderLineID=@p2 AND ISNULL(IsClosed,0)=0
|
|||||||
id, meta.item, meta.color, meta.dim1, meta.dim2)
|
id, meta.item, meta.color, meta.dim1, meta.dim2)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, err := tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLineCurrency WHERE OrderLineID=@p1`, id); err != nil {
|
if err := deleteOrSoftCloseLine(id); err != nil {
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, err := tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLine WHERE OrderLineID=@p1 AND ISNULL(IsClosed,0)=0`, id); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,74 +10,135 @@ 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 Opening AS (
|
;WITH CurrDesc 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 (
|
||||||
AND EXISTS (
|
(hm.HasMov = 1 AND b.DocumentDate < @startdate) -- hareket varsa: klasik devir
|
||||||
SELECT 1
|
OR (hm.HasMov = 0 AND b.DocumentDate <= @enddate) -- hareket yoksa: enddate itibariyle bakiye
|
||||||
FROM CurrAccBookATAttributesFilter f2
|
|
||||||
WHERE f2.CurrAccBookID = b.CurrAccBookID
|
|
||||||
AND f2.ATAtt01 IN (%s)
|
|
||||||
)
|
)
|
||||||
GROUP BY b.CurrAccCode, b.DocCurrencyCode
|
GROUP BY 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,
|
||||||
|
|
||||||
b.RefNumber AS Belge_No,
|
b.RefNumber AS Belge_No,
|
||||||
b.BaseApplicationCode AS Islem_Tipi,
|
b.BaseApplicationCode AS Islem_Tipi,
|
||||||
b.LineDescription AS Aciklama,
|
b.LineDescription AS Aciklama,
|
||||||
|
|
||||||
b.DocCurrencyCode AS Para_Birimi,
|
b.DocCurrencyCode AS Para_Birimi,
|
||||||
c.Debit AS Borc,
|
|
||||||
c.Credit AS Alacak,
|
ISNULL(c.Debit,0) AS Borc,
|
||||||
SUM(c.Debit - c.Credit)
|
ISNULL(c.Credit,0) AS Alacak,
|
||||||
OVER (PARTITION BY b.CurrAccCode, c.CurrencyCode
|
|
||||||
ORDER BY b.DocumentDate, b.CurrAccBookID) AS Hareket_Bakiyesi,
|
SUM(ISNULL(c.Debit,0) - ISNULL(c.Credit,0))
|
||||||
|
OVER (
|
||||||
|
PARTITION BY b.DocCurrencyCode
|
||||||
|
ORDER BY b.DocumentDate, b.CurrAccBookID
|
||||||
|
) AS Hareket_Bakiyesi,
|
||||||
|
|
||||||
f.ATAtt01 AS Parislemtipi
|
f.ATAtt01 AS Parislemtipi
|
||||||
|
|
||||||
FROM trCurrAccBook b
|
FROM trCurrAccBook b
|
||||||
LEFT JOIN cdCurrAccDesc d
|
INNER JOIN CurrAccBookATAttributesFilter f
|
||||||
ON b.CurrAccCode = d.CurrAccCode
|
ON f.CurrAccBookID = b.CurrAccBookID
|
||||||
|
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
|
||||||
m.Cari_Kod,
|
m.Cari_Kod,
|
||||||
m.Cari_Isim,
|
m.Cari_Isim,
|
||||||
@@ -89,8 +150,12 @@ SELECT
|
|||||||
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
|
||||||
@@ -98,43 +163,49 @@ LEFT JOIN Opening o
|
|||||||
|
|
||||||
UNION ALL
|
UNION ALL
|
||||||
|
|
||||||
-- Devir satırı
|
/* =========================================================
|
||||||
|
✅ Devir Satırı (kur bazında) — Opening'den gelir
|
||||||
|
Hareket varsa: startdate öncesi
|
||||||
|
Hareket yoksa: enddate itibariyle bakiye
|
||||||
|
========================================================= */
|
||||||
SELECT
|
SELECT
|
||||||
@Carikod AS Cari_Kod,
|
o.Cari_Kod,
|
||||||
MAX(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), @startdate, 23) AS Belge_Tarihi,
|
CONVERT(varchar(10), @startdate, 23) AS Belge_Tarihi,
|
||||||
CONVERT(varchar(10), @startdate, 23) AS Vade_Tarihi,
|
CONVERT(varchar(10), @startdate, 23) AS Vade_Tarihi,
|
||||||
|
|
||||||
'Baslangic_devir' AS Belge_No,
|
'Baslangic_devir' AS Belge_No,
|
||||||
'Devir' AS Islem_Tipi,
|
'Devir' AS Islem_Tipi,
|
||||||
'Devir Bakiyesi' AS Aciklama,
|
'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
|
|
||||||
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;
|
o.Para_Birimi,
|
||||||
`, parislemFilter, parislemFilter, parislemFilter)
|
|
||||||
|
CASE WHEN o.Devir_Bakiyesi >= 0 THEN o.Devir_Bakiyesi ELSE 0 END AS Borc,
|
||||||
|
CASE WHEN o.Devir_Bakiyesi < 0 THEN ABS(o.Devir_Bakiyesi) ELSE 0 END AS Alacak,
|
||||||
|
|
||||||
|
o.Devir_Bakiyesi AS Bakiye,
|
||||||
|
|
||||||
|
CAST(NULL AS varchar(32)) AS Parislemler
|
||||||
|
|
||||||
|
FROM Opening o
|
||||||
|
|
||||||
|
ORDER BY
|
||||||
|
Para_Birimi,
|
||||||
|
Belge_Tarihi;
|
||||||
|
`,
|
||||||
|
parislemFilter, // HasMovement
|
||||||
|
parislemFilter, // Opening
|
||||||
|
parislemFilter, // Movements
|
||||||
|
)
|
||||||
|
|
||||||
rows, err := db.MssqlDB.Query(query,
|
rows, err := db.MssqlDB.Query(query,
|
||||||
sql.Named("startdate", params.StartDate),
|
sql.Named("startdate", params.StartDate),
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
35
svc/queries/statement_pdf_common.go
Normal file
35
svc/queries/statement_pdf_common.go
Normal 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
|
||||||
|
}
|
||||||
@@ -32,8 +32,16 @@ SELECT
|
|||||||
a.ItemCode AS Urun_Kodu,
|
a.ItemCode AS Urun_Kodu,
|
||||||
a.ColorCode AS Urun_Rengi,
|
a.ColorCode AS Urun_Rengi,
|
||||||
SUM(a.Qty1) AS Toplam_Adet,
|
SUM(a.Qty1) AS Toplam_Adet,
|
||||||
SUM(ABS(a.Doc_Price)) AS Toplam_Fiyat,
|
|
||||||
CAST(SUM(a.Qty1 * ABS(a.Doc_Price)) AS numeric(18,2)) AS Toplam_Tutar
|
CAST(
|
||||||
|
SUM(a.Qty1 * ABS(a.Doc_Price))
|
||||||
|
/ NULLIF(SUM(a.Qty1),0)
|
||||||
|
AS numeric(18,4)) AS Doviz_Fiyat,
|
||||||
|
|
||||||
|
CAST(
|
||||||
|
SUM(a.Qty1 * ABS(a.Doc_Price))
|
||||||
|
AS numeric(18,2)) AS Toplam_Tutar
|
||||||
|
|
||||||
FROM AllInvoicesWithAttributes a
|
FROM AllInvoicesWithAttributes a
|
||||||
LEFT JOIN prItemAttribute AnaGrup
|
LEFT JOIN prItemAttribute AnaGrup
|
||||||
ON a.ItemCode = AnaGrup.ItemCode AND AnaGrup.AttributeTypeCode = 1
|
ON a.ItemCode = AnaGrup.ItemCode AND AnaGrup.AttributeTypeCode = 1
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ SET
|
|||||||
full_name = $4,
|
full_name = $4,
|
||||||
email = $5,
|
email = $5,
|
||||||
mobile = $6,
|
mobile = $6,
|
||||||
address = NULLIF($7, ''),
|
address = COALESCE($7, ''),
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
// ---------------------------------------------------
|
// ---------------------------------------------------
|
||||||
|
|||||||
@@ -6,10 +6,13 @@ import (
|
|||||||
"bssapp-backend/internal/security"
|
"bssapp-backend/internal/security"
|
||||||
"bssapp-backend/models"
|
"bssapp-backend/models"
|
||||||
"bssapp-backend/repository"
|
"bssapp-backend/repository"
|
||||||
|
"bssapp-backend/services"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
@@ -17,86 +20,128 @@ import (
|
|||||||
|
|
||||||
func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
// --------------------------------------------------
|
|
||||||
// 1️⃣ JWT CLAIMS
|
|
||||||
// --------------------------------------------------
|
|
||||||
claims, ok := auth.GetClaimsFromContext(r.Context())
|
claims, ok := auth.GetClaimsFromContext(r.Context())
|
||||||
if !ok || claims == nil {
|
if !ok || claims == nil {
|
||||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
log.Printf(
|
||||||
|
"FIRST_PASSWORD_CHANGE 401 reason=claims_missing method=%s path=%s",
|
||||||
|
r.Method,
|
||||||
|
r.URL.Path,
|
||||||
|
)
|
||||||
|
http.Error(w, "yetkisiz: token eksik veya geçersiz", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------
|
|
||||||
// 2️⃣ PAYLOAD
|
|
||||||
// --------------------------------------------------
|
|
||||||
var req struct {
|
var req struct {
|
||||||
CurrentPassword string `json:"current_password"`
|
CurrentPassword string `json:"current_password"`
|
||||||
NewPassword string `json:"new_password"`
|
NewPassword string `json:"new_password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
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.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
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------
|
mkRepo := repository.NewMkUserRepository(db)
|
||||||
// 3️⃣ LOAD USER (mk_dfusr)
|
legacyRepo := repository.NewUserRepository(db)
|
||||||
// --------------------------------------------------
|
|
||||||
var currentHash string
|
|
||||||
err := db.QueryRow(`
|
|
||||||
SELECT password_hash
|
|
||||||
FROM mk_dfusr
|
|
||||||
WHERE id = $1
|
|
||||||
`, claims.ID).Scan(¤tHash)
|
|
||||||
|
|
||||||
if err != nil || currentHash == "" {
|
mkUser, mkErr := mkRepo.GetByID(claims.ID)
|
||||||
http.Error(w, "user not found", http.StatusUnauthorized)
|
hasMkUser := mkErr == nil
|
||||||
|
if mkErr != nil && !errors.Is(mkErr, repository.ErrMkUserNotFound) {
|
||||||
|
log.Printf(
|
||||||
|
"FIRST_PASSWORD_CHANGE 500 reason=mk_lookup_failed user_id=%d err=%v",
|
||||||
|
claims.ID,
|
||||||
|
mkErr,
|
||||||
|
)
|
||||||
|
http.Error(w, "kullanıcı sorgulama hatası", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------
|
var legacyUser *models.User
|
||||||
// 4️⃣ CURRENT PASSWORD CHECK
|
|
||||||
// --------------------------------------------------
|
// If user already exists in mk_dfusr with hash, verify against mk hash.
|
||||||
|
// Otherwise verify against legacy dfusr password before migration.
|
||||||
|
if hasMkUser && strings.TrimSpace(mkUser.PasswordHash) != "" {
|
||||||
if bcrypt.CompareHashAndPassword(
|
if bcrypt.CompareHashAndPassword(
|
||||||
[]byte(currentHash),
|
[]byte(mkUser.PasswordHash),
|
||||||
[]byte(req.CurrentPassword),
|
[]byte(req.CurrentPassword),
|
||||||
) != nil {
|
) != nil {
|
||||||
|
log.Printf(
|
||||||
|
"FIRST_PASSWORD_CHANGE 401 reason=current_password_mismatch_mk user_id=%d username=%s",
|
||||||
|
claims.ID,
|
||||||
|
claims.Username,
|
||||||
|
)
|
||||||
http.Error(w, "mevcut şifre hatalı", http.StatusUnauthorized)
|
http.Error(w, "mevcut şifre hatalı", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
legacyUser, err = legacyRepo.GetLegacyUserForLogin(claims.Username)
|
||||||
|
if err != nil || legacyUser == nil || !legacyUser.IsActive {
|
||||||
|
log.Printf(
|
||||||
|
"FIRST_PASSWORD_CHANGE 401 reason=legacy_user_not_found user_id=%d username=%s err=%v",
|
||||||
|
claims.ID,
|
||||||
|
claims.Username,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if !services.CheckPasswordWithLegacy(legacyUser, req.CurrentPassword) {
|
||||||
|
log.Printf(
|
||||||
|
"FIRST_PASSWORD_CHANGE 401 reason=current_password_mismatch_legacy user_id=%d username=%s",
|
||||||
|
claims.ID,
|
||||||
|
claims.Username,
|
||||||
|
)
|
||||||
|
http.Error(w, "mevcut şifre hatalı", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------
|
|
||||||
// 5️⃣ PASSWORD POLICY
|
|
||||||
// --------------------------------------------------
|
|
||||||
if err := security.ValidatePassword(req.NewPassword); err != nil {
|
if err := security.ValidatePassword(req.NewPassword); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------
|
|
||||||
// 6️⃣ HASH NEW PASSWORD
|
|
||||||
// --------------------------------------------------
|
|
||||||
hash, err := bcrypt.GenerateFromPassword(
|
hash, err := bcrypt.GenerateFromPassword(
|
||||||
[]byte(req.NewPassword),
|
[]byte(req.NewPassword),
|
||||||
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()
|
||||||
// 7️⃣ UPDATE mk_dfusr
|
if err != nil {
|
||||||
// --------------------------------------------------
|
http.Error(w, "işlem başlatılamadı", http.StatusInternalServerError)
|
||||||
_, err = db.Exec(`
|
return
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
migratedFromLegacy := false
|
||||||
|
|
||||||
|
if hasMkUser {
|
||||||
|
res, err := tx.Exec(`
|
||||||
UPDATE mk_dfusr
|
UPDATE mk_dfusr
|
||||||
SET
|
SET
|
||||||
password_hash = $1,
|
password_hash = $1,
|
||||||
@@ -105,27 +150,124 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $2
|
WHERE id = $2
|
||||||
`, string(hash), claims.ID)
|
`, string(hash), claims.ID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "password update failed", http.StatusInternalServerError)
|
log.Printf(
|
||||||
|
"FIRST_PASSWORD_CHANGE 500 reason=password_update_failed user_id=%d err=%v",
|
||||||
|
claims.ID,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
http.Error(w, "şifre güncellenemedi", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------
|
affected, _ := res.RowsAffected()
|
||||||
// 8️⃣ REFRESH TOKEN REVOKE
|
if affected == 0 {
|
||||||
// --------------------------------------------------
|
log.Printf(
|
||||||
_ = repository.
|
"FIRST_PASSWORD_CHANGE 500 reason=password_update_no_rows user_id=%d",
|
||||||
NewRefreshTokenRepository(db).
|
claims.ID,
|
||||||
RevokeAllForUser(claims.ID)
|
)
|
||||||
|
http.Error(w, "şifre güncellenemedi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if legacyUser == nil {
|
||||||
|
// Defensive fallback, should not happen.
|
||||||
|
legacyUser, err = legacyRepo.GetLegacyUserForLogin(claims.Username)
|
||||||
|
if err != nil || legacyUser == nil {
|
||||||
|
log.Printf(
|
||||||
|
"FIRST_PASSWORD_CHANGE 500 reason=legacy_reload_failed user_id=%d username=%s err=%v",
|
||||||
|
claims.ID,
|
||||||
|
claims.Username,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
INSERT INTO mk_dfusr (
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
full_name,
|
||||||
|
mobile,
|
||||||
|
address,
|
||||||
|
is_active,
|
||||||
|
password_hash,
|
||||||
|
force_password_change,
|
||||||
|
password_updated_at,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
$1,$2,$3,$4,$5,$6,$7,$8,false,NOW(),NOW(),NOW()
|
||||||
|
)
|
||||||
|
ON CONFLICT (id)
|
||||||
|
DO UPDATE SET
|
||||||
|
username = EXCLUDED.username,
|
||||||
|
email = EXCLUDED.email,
|
||||||
|
full_name = EXCLUDED.full_name,
|
||||||
|
mobile = EXCLUDED.mobile,
|
||||||
|
address = EXCLUDED.address,
|
||||||
|
is_active = EXCLUDED.is_active,
|
||||||
|
password_hash = EXCLUDED.password_hash,
|
||||||
|
force_password_change = false,
|
||||||
|
password_updated_at = NOW(),
|
||||||
|
updated_at = NOW()
|
||||||
|
`,
|
||||||
|
int64(legacyUser.ID),
|
||||||
|
strings.TrimSpace(legacyUser.Username),
|
||||||
|
strings.TrimSpace(legacyUser.Email),
|
||||||
|
strings.TrimSpace(legacyUser.FullName),
|
||||||
|
strings.TrimSpace(legacyUser.Mobile),
|
||||||
|
strings.TrimSpace(legacyUser.Address),
|
||||||
|
legacyUser.IsActive,
|
||||||
|
string(hash),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(
|
||||||
|
"FIRST_PASSWORD_CHANGE 500 reason=legacy_migration_failed user_id=%d username=%s err=%v",
|
||||||
|
claims.ID,
|
||||||
|
claims.Username,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
http.Error(w, "legacy geçişi başarısız", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
migratedFromLegacy = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
log.Printf(
|
||||||
|
"FIRST_PASSWORD_CHANGE 500 reason=tx_commit_failed user_id=%d err=%v",
|
||||||
|
claims.ID,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
http.Error(w, "işlem tamamlanamadı", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = repository.NewRefreshTokenRepository(db).RevokeAllForUser(claims.ID)
|
||||||
|
|
||||||
// --------------------------------------------------
|
|
||||||
// 9️⃣ NEW JWT (TEK DOĞRU YOL)
|
|
||||||
// --------------------------------------------------
|
|
||||||
newClaims := auth.BuildClaimsFromUser(
|
newClaims := auth.BuildClaimsFromUser(
|
||||||
&models.MkUser{
|
&models.MkUser{
|
||||||
ID: claims.ID,
|
ID: claims.ID,
|
||||||
Username: claims.Username,
|
Username: claims.Username,
|
||||||
|
RoleID: claims.RoleID,
|
||||||
RoleCode: claims.RoleCode,
|
RoleCode: claims.RoleCode,
|
||||||
|
DepartmentCodes: claims.DepartmentCodes,
|
||||||
V3Username: claims.V3Username,
|
V3Username: claims.V3Username,
|
||||||
V3UserGroup: claims.V3UserGroup,
|
V3UserGroup: claims.V3UserGroup,
|
||||||
SessionID: claims.SessionID,
|
SessionID: claims.SessionID,
|
||||||
@@ -140,22 +282,20 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------
|
source := "mk_password_update"
|
||||||
// 🔟 AUDIT
|
if migratedFromLegacy {
|
||||||
// --------------------------------------------------
|
source = "legacy_migration_completed"
|
||||||
|
}
|
||||||
auditlog.ForcePasswordChangeCompleted(
|
auditlog.ForcePasswordChangeCompleted(
|
||||||
r.Context(),
|
r.Context(),
|
||||||
claims.ID,
|
claims.ID,
|
||||||
"self_change",
|
source,
|
||||||
)
|
)
|
||||||
|
|
||||||
// --------------------------------------------------
|
|
||||||
// 1️⃣1️⃣ RESPONSE
|
|
||||||
// --------------------------------------------------
|
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
"token": newToken,
|
"token": newToken,
|
||||||
"user": map[string]any{
|
"user": map[string]any{
|
||||||
@@ -163,7 +303,14 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
"username": claims.Username,
|
"username": claims.Username,
|
||||||
"force_password_change": false,
|
"force_password_change": false,
|
||||||
},
|
},
|
||||||
|
"migrated_from_legacy": migratedFromLegacy,
|
||||||
})
|
})
|
||||||
log.Printf("✅ FIRST-PASS claims user=%d role=%s", claims.ID, claims.RoleCode)
|
|
||||||
|
log.Printf(
|
||||||
|
"FIRST_PASSWORD_CHANGE 200 user=%d role=%s migrated_from_legacy=%v",
|
||||||
|
claims.ID,
|
||||||
|
claims.RoleCode,
|
||||||
|
migratedFromLegacy,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package routes
|
|||||||
import (
|
import (
|
||||||
"bssapp-backend/auth"
|
"bssapp-backend/auth"
|
||||||
"bssapp-backend/internal/auditlog"
|
"bssapp-backend/internal/auditlog"
|
||||||
|
"bssapp-backend/internal/security"
|
||||||
"bssapp-backend/models"
|
"bssapp-backend/models"
|
||||||
"bssapp-backend/queries"
|
"bssapp-backend/queries"
|
||||||
"bssapp-backend/repository"
|
"bssapp-backend/repository"
|
||||||
@@ -29,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) {
|
||||||
|
|
||||||
@@ -83,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)
|
||||||
@@ -133,31 +221,30 @@ func LoginHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================================================
|
// ==================================================
|
||||||
// 3️⃣ MIGRATION (dfusr → mk_dfusr)
|
// 3️⃣ LEGACY SESSION (PENDING MIGRATION)
|
||||||
|
// - mk_dfusr migration is completed in /api/password/change
|
||||||
// ==================================================
|
// ==================================================
|
||||||
newHash, err := bcrypt.GenerateFromPassword(
|
mkID, err := ensureLegacyUserReadyForSession(db, legacyUser)
|
||||||
[]byte(pass),
|
|
||||||
bcrypt.DefaultCost,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Şifre üretilemedi", http.StatusInternalServerError)
|
log.Printf("LEGACY LOGIN MIGRATION BIND FAILED username=%s err=%v", login, err)
|
||||||
|
http.Error(w, "Giriş yapılamadı", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mkUser, err = mkRepo.CreateFromLegacy(legacyUser, string(newHash))
|
mkUser = &models.MkUser{
|
||||||
if err != nil {
|
ID: mkID,
|
||||||
log.Println("❌ CREATE_FROM_LEGACY FAILED:", err)
|
Username: legacyUser.Username,
|
||||||
http.Error(w, "Kullanıcı migrate edilemedi", http.StatusInternalServerError)
|
Email: legacyUser.Email,
|
||||||
return
|
IsActive: legacyUser.IsActive,
|
||||||
|
RoleID: int64(legacyUser.RoleID),
|
||||||
|
RoleCode: legacyUser.RoleCode,
|
||||||
|
ForcePasswordChange: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 KRİTİK: TOKEN GUARD İÇİN GARANTİ
|
|
||||||
mkUser.ForcePasswordChange = true
|
|
||||||
|
|
||||||
auditlog.Write(auditlog.ActivityLog{
|
auditlog.Write(auditlog.ActivityLog{
|
||||||
ActionType: "LEGACY_USER_MIGRATED",
|
ActionType: "LEGACY_USER_LOGIN_PENDING_MIGRATION",
|
||||||
ActionCategory: "security",
|
ActionCategory: "security",
|
||||||
Description: "dfusr -> mk_dfusr on login",
|
Description: "legacy giriş başarılı, ilk şifre değişikliği gerekli",
|
||||||
IsSuccess: true,
|
IsSuccess: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -216,6 +303,22 @@ func writeLoginResponse(w http.ResponseWriter, db *sql.DB, user *models.MkUser)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshPlain, refreshHash, err := security.GenerateRefreshToken()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Refresh token üretilemedi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshExp := time.Now().Add(14 * 24 * time.Hour)
|
||||||
|
rtRepo := repository.NewRefreshTokenRepository(db)
|
||||||
|
if err := rtRepo.IssueRefreshToken(user.ID, refreshHash, refreshExp); err != nil {
|
||||||
|
log.Printf("refresh token store failed user=%d err=%v", user.ID, err)
|
||||||
|
http.Error(w, "Session başlatılamadı", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setRefreshCookie(w, refreshPlain, refreshExp)
|
||||||
|
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
"token": token,
|
"token": token,
|
||||||
"user": map[string]any{
|
"user": map[string]any{
|
||||||
|
|||||||
@@ -3,16 +3,19 @@ package routes
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/jung-kurt/gofpdf"
|
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"runtime/debug"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/jung-kurt/gofpdf"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* ===========================================================
|
/* ===========================================================
|
||||||
@@ -238,6 +241,13 @@ func s64(v sql.NullString) string {
|
|||||||
return v.String
|
return v.String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sOrEmpty(v sql.NullString) string {
|
||||||
|
if !v.Valid {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(v.String)
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeBedenLabelGo(v string) string {
|
func normalizeBedenLabelGo(v string) string {
|
||||||
// 1️⃣ NULL / boş / whitespace → " " (aksbir null kolonu)
|
// 1️⃣ NULL / boş / whitespace → " " (aksbir null kolonu)
|
||||||
s := strings.TrimSpace(v)
|
s := strings.TrimSpace(v)
|
||||||
@@ -281,25 +291,88 @@ func normalizeBedenLabelGo(v string) string {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseNumericSize(v string) (int, bool) {
|
||||||
|
s := strings.TrimSpace(strings.ToUpper(v))
|
||||||
|
if s == "" {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
n, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return n, true
|
||||||
|
}
|
||||||
|
|
||||||
func detectBedenGroupGo(bedenList []string, ana, alt string) string {
|
func detectBedenGroupGo(bedenList []string, ana, alt string) string {
|
||||||
ana = safeTrimUpper(ana)
|
ana = safeTrimUpper(ana)
|
||||||
alt = safeTrimUpper(alt)
|
alt = safeTrimUpper(alt)
|
||||||
|
|
||||||
|
// Ürün grubu adı doğrudan ayakkabı ise öncelikli.
|
||||||
|
if strings.Contains(ana, "AYAKKABI") || strings.Contains(alt, "AYAKKABI") {
|
||||||
|
return catAyk
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasYasNumeric bool
|
||||||
|
var hasAykNumeric bool
|
||||||
|
var hasPanNumeric bool
|
||||||
|
|
||||||
for _, b := range bedenList {
|
for _, b := range bedenList {
|
||||||
|
b = safeTrimUpper(b)
|
||||||
|
|
||||||
switch b {
|
switch b {
|
||||||
case "XS", "S", "M", "L", "XL":
|
case "XS", "S", "M", "L", "XL",
|
||||||
|
"2XL", "3XL", "4XL", "5XL", "6XL", "7XL":
|
||||||
return catGom
|
return catGom
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if n, ok := parseNumericSize(b); ok {
|
||||||
|
if n >= 2 && n <= 14 {
|
||||||
|
hasYasNumeric = true
|
||||||
|
}
|
||||||
|
if n >= 39 && n <= 45 {
|
||||||
|
hasAykNumeric = true
|
||||||
|
}
|
||||||
|
if n >= 38 && n <= 68 {
|
||||||
|
hasPanNumeric = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasAykNumeric {
|
||||||
|
return catAyk
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(ana, "PANTOLON") {
|
if strings.Contains(ana, "PANTOLON") {
|
||||||
return catPan
|
return catPan
|
||||||
}
|
}
|
||||||
|
if hasPanNumeric {
|
||||||
|
return catPan
|
||||||
|
}
|
||||||
if strings.Contains(alt, "ÇOCUK") || strings.Contains(alt, "GARSON") {
|
if strings.Contains(alt, "ÇOCUK") || strings.Contains(alt, "GARSON") {
|
||||||
return catYas
|
return catYas
|
||||||
}
|
}
|
||||||
|
if hasYasNumeric {
|
||||||
|
return catYas
|
||||||
|
}
|
||||||
|
|
||||||
return catTak
|
return catTak
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formatSizeQtyForLog(m map[string]int) string {
|
||||||
|
if len(m) == 0 {
|
||||||
|
return "{}"
|
||||||
|
}
|
||||||
|
keys := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
parts := make([]string, 0, len(keys))
|
||||||
|
for _, k := range keys {
|
||||||
|
parts = append(parts, fmt.Sprintf("%s:%d", k, m[k]))
|
||||||
|
}
|
||||||
|
return "{" + strings.Join(parts, ", ") + "}"
|
||||||
|
}
|
||||||
func defaultSizeListFor(cat string) []string {
|
func defaultSizeListFor(cat string) []string {
|
||||||
switch cat {
|
switch cat {
|
||||||
case catAyk:
|
case catAyk:
|
||||||
@@ -331,25 +404,25 @@ func contains(list []string, v string) bool {
|
|||||||
2) PDF OLUŞTURUCU (A4 YATAY + FOOTER)
|
2) PDF OLUŞTURUCU (A4 YATAY + FOOTER)
|
||||||
=========================================================== */
|
=========================================================== */
|
||||||
|
|
||||||
func newOrderPdf() *gofpdf.Fpdf {
|
func newOrderPdf() (*gofpdf.Fpdf, error) {
|
||||||
pdf := gofpdf.New("L", "mm", "A4", "")
|
pdf := gofpdf.New("L", "mm", "A4", "")
|
||||||
pdf.SetMargins(10, 10, 10)
|
pdf.SetMargins(10, 10, 10)
|
||||||
pdf.SetAutoPageBreak(false, 12)
|
pdf.SetAutoPageBreak(false, 12)
|
||||||
|
|
||||||
// UTF8 fontlar
|
if err := registerDejavuFonts(pdf, "dejavu"); err != nil {
|
||||||
pdf.AddUTF8Font("dejavu", "", "fonts/DejaVuSans.ttf")
|
return nil, err
|
||||||
pdf.AddUTF8Font("dejavu-b", "", "fonts/DejaVuSans-Bold.ttf")
|
}
|
||||||
|
|
||||||
// Footer: sayfa numarası
|
// Footer: sayfa numarası
|
||||||
pdf.AliasNbPages("")
|
pdf.AliasNbPages("")
|
||||||
pdf.SetFooterFunc(func() {
|
pdf.SetFooterFunc(func() {
|
||||||
pdf.SetY(-10)
|
pdf.SetY(-10)
|
||||||
pdf.SetFont("dejavu", "", 8)
|
pdf.SetFont("dejavu", "B", 8)
|
||||||
txt := fmt.Sprintf("Sayfa %d/{nb}", pdf.PageNo())
|
txt := fmt.Sprintf("Sayfa %d/{nb}", pdf.PageNo())
|
||||||
pdf.CellFormat(0, 10, txt, "", 0, "R", false, 0, "")
|
pdf.CellFormat(0, 10, txt, "", 0, "R", false, 0, "")
|
||||||
})
|
})
|
||||||
|
|
||||||
return pdf
|
return pdf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===========================================================
|
/* ===========================================================
|
||||||
@@ -373,7 +446,7 @@ func getOrderHeaderFromDB(db *sql.DB, orderID string) (*OrderHeader, error) {
|
|||||||
ISNULL((
|
ISNULL((
|
||||||
SELECT TOP (1) ca.AttributeDescription
|
SELECT TOP (1) ca.AttributeDescription
|
||||||
FROM BAGGI_V3.dbo.cdCurrAccAttributeDesc AS ca WITH (NOLOCK)
|
FROM BAGGI_V3.dbo.cdCurrAccAttributeDesc AS ca WITH (NOLOCK)
|
||||||
WHERE ca.CurrAccTypeCode = 3
|
WHERE ca.CurrAccTypeCode IN (1,3)
|
||||||
AND ca.AttributeTypeCode = 2 -- 🟡 Müşteri Temsilcisi
|
AND ca.AttributeTypeCode = 2 -- 🟡 Müşteri Temsilcisi
|
||||||
AND ca.AttributeCode = f.CustomerAtt02
|
AND ca.AttributeCode = f.CustomerAtt02
|
||||||
AND ca.LangCode = 'TR'
|
AND ca.LangCode = 'TR'
|
||||||
@@ -388,24 +461,36 @@ func getOrderHeaderFromDB(db *sql.DB, orderID string) (*OrderHeader, error) {
|
|||||||
|
|
||||||
var h OrderHeader
|
var h OrderHeader
|
||||||
var orderDate sql.NullTime
|
var orderDate sql.NullTime
|
||||||
|
var orderNumber, currAccCode, currAccName, docCurrency sql.NullString
|
||||||
|
var description, internalDesc, officeCode, createdUser, customerRep sql.NullString
|
||||||
|
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&h.OrderHeaderID,
|
&h.OrderHeaderID,
|
||||||
&h.OrderNumber,
|
&orderNumber,
|
||||||
&h.CurrAccCode,
|
&currAccCode,
|
||||||
&h.CurrAccName,
|
&currAccName,
|
||||||
&h.DocCurrency,
|
&docCurrency,
|
||||||
&orderDate,
|
&orderDate,
|
||||||
&h.Description,
|
&description,
|
||||||
&h.InternalDesc,
|
&internalDesc,
|
||||||
&h.OfficeCode,
|
&officeCode,
|
||||||
&h.CreatedUser,
|
&createdUser,
|
||||||
&h.CustomerRep, // 🆕 buradan geliyor
|
&customerRep, // 🆕 buradan geliyor
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.OrderNumber = sOrEmpty(orderNumber)
|
||||||
|
h.CurrAccCode = sOrEmpty(currAccCode)
|
||||||
|
h.CurrAccName = sOrEmpty(currAccName)
|
||||||
|
h.DocCurrency = sOrEmpty(docCurrency)
|
||||||
|
h.Description = sOrEmpty(description)
|
||||||
|
h.InternalDesc = sOrEmpty(internalDesc)
|
||||||
|
h.OfficeCode = sOrEmpty(officeCode)
|
||||||
|
h.CreatedUser = sOrEmpty(createdUser)
|
||||||
|
h.CustomerRep = sOrEmpty(customerRep)
|
||||||
|
|
||||||
if orderDate.Valid {
|
if orderDate.Valid {
|
||||||
h.OrderDate = orderDate.Time
|
h.OrderDate = orderDate.Time
|
||||||
}
|
}
|
||||||
@@ -626,9 +711,8 @@ func drawOrderHeader(pdf *gofpdf.Fpdf, h *OrderHeader, showDesc bool) float64 {
|
|||||||
/* ----------------------------------------------------
|
/* ----------------------------------------------------
|
||||||
1) LOGO
|
1) LOGO
|
||||||
---------------------------------------------------- */
|
---------------------------------------------------- */
|
||||||
logo := "./public/Baggi-Tekstil-A.s-Logolu.jpeg"
|
if logoPath, err := resolvePdfImagePath("Baggi-Tekstil-A.s-Logolu.jpeg"); err == nil {
|
||||||
if _, err := os.Stat(logo); err == nil {
|
pdf.ImageOptions(logoPath, marginL, y, 32, 0, false, gofpdf.ImageOptions{}, 0, "")
|
||||||
pdf.ImageOptions(logo, marginL, y, 32, 0, false, gofpdf.ImageOptions{}, 0, "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
/* ----------------------------------------------------
|
||||||
@@ -641,7 +725,7 @@ func drawOrderHeader(pdf *gofpdf.Fpdf, h *OrderHeader, showDesc bool) float64 {
|
|||||||
pdf.SetFillColor(149, 113, 22) // Baggi altın
|
pdf.SetFillColor(149, 113, 22) // Baggi altın
|
||||||
pdf.Rect(titleX, titleY, titleW, 10, "F")
|
pdf.Rect(titleX, titleY, titleW, 10, "F")
|
||||||
|
|
||||||
pdf.SetFont("dejavu-b", "", 13)
|
pdf.SetFont("dejavu", "B", 13)
|
||||||
pdf.SetTextColor(255, 255, 255)
|
pdf.SetTextColor(255, 255, 255)
|
||||||
pdf.SetXY(titleX+4, titleY+2)
|
pdf.SetXY(titleX+4, titleY+2)
|
||||||
pdf.CellFormat(titleW-8, 6, "BAGGI TEKSTİL - SİPARİŞ FORMU", "", 0, "L", false, 0, "")
|
pdf.CellFormat(titleW-8, 6, "BAGGI TEKSTİL - SİPARİŞ FORMU", "", 0, "L", false, 0, "")
|
||||||
@@ -657,7 +741,7 @@ func drawOrderHeader(pdf *gofpdf.Fpdf, h *OrderHeader, showDesc bool) float64 {
|
|||||||
pdf.SetDrawColor(180, 180, 180)
|
pdf.SetDrawColor(180, 180, 180)
|
||||||
pdf.Rect(boxX, boxY, boxW, boxH, "")
|
pdf.Rect(boxX, boxY, boxW, boxH, "")
|
||||||
|
|
||||||
pdf.SetFont("dejavu-b", "", 9)
|
pdf.SetFont("dejavu", "B", 9)
|
||||||
pdf.SetTextColor(149, 113, 22)
|
pdf.SetTextColor(149, 113, 22)
|
||||||
rep := strings.TrimSpace(h.CustomerRep)
|
rep := strings.TrimSpace(h.CustomerRep)
|
||||||
if rep == "" {
|
if rep == "" {
|
||||||
@@ -712,7 +796,7 @@ func drawOrderHeader(pdf *gofpdf.Fpdf, h *OrderHeader, showDesc bool) float64 {
|
|||||||
pdf.Rect(marginL, y, pageW-marginL*2, descBoxH, "")
|
pdf.Rect(marginL, y, pageW-marginL*2, descBoxH, "")
|
||||||
|
|
||||||
// Başlık
|
// Başlık
|
||||||
pdf.SetFont("dejavu-b", "", 8)
|
pdf.SetFont("dejavu", "B", 8)
|
||||||
pdf.SetTextColor(149, 113, 22)
|
pdf.SetTextColor(149, 113, 22)
|
||||||
pdf.SetXY(marginL+3, y+2)
|
pdf.SetXY(marginL+3, y+2)
|
||||||
pdf.CellFormat(40, 4, "Sipariş Genel Açıklaması:", "", 0, "L", false, 0, "")
|
pdf.CellFormat(40, 4, "Sipariş Genel Açıklaması:", "", 0, "L", false, 0, "")
|
||||||
@@ -740,7 +824,7 @@ func drawOrderHeader(pdf *gofpdf.Fpdf, h *OrderHeader, showDesc bool) float64 {
|
|||||||
===========================================================
|
===========================================================
|
||||||
*/
|
*/
|
||||||
func drawGridHeader(pdf *gofpdf.Fpdf, layout pdfLayout, startY float64, catSizes CategorySizeMap) float64 {
|
func drawGridHeader(pdf *gofpdf.Fpdf, layout pdfLayout, startY float64, catSizes CategorySizeMap) float64 {
|
||||||
pdf.SetFont("dejavu-b", "", 6)
|
pdf.SetFont("dejavu", "B", 6)
|
||||||
pdf.SetDrawColor(baggiGrayBorderR, baggiGrayBorderG, baggiGrayBorderB)
|
pdf.SetDrawColor(baggiGrayBorderR, baggiGrayBorderG, baggiGrayBorderB)
|
||||||
pdf.SetFillColor(baggiCreamR, baggiCreamG, baggiCreamB)
|
pdf.SetFillColor(baggiCreamR, baggiCreamG, baggiCreamB)
|
||||||
pdf.SetTextColor(20, 20, 20) // 🟣 TÜM HEADER YAZILARI SİYAH
|
pdf.SetTextColor(20, 20, 20) // 🟣 TÜM HEADER YAZILARI SİYAH
|
||||||
@@ -896,10 +980,17 @@ func calcRowHeight(pdf *gofpdf.Fpdf, layout pdfLayout, row PdfRow) float64 {
|
|||||||
return base
|
return base
|
||||||
}
|
}
|
||||||
|
|
||||||
// Yeni: açıklama genişliği = sol + sağ
|
// Açıklama tek kolonda (ColDescLeft) render ediliyor.
|
||||||
descW := layout.ColDescW
|
// ColDescW set edilmediği için 0 kalabiliyor; bu durumda SplitLines patlayabiliyor.
|
||||||
|
descW := layout.ColDescLeft
|
||||||
|
if descW <= float64(2*OcellPadX) {
|
||||||
|
descW = layout.ColDescLeft + layout.ColDescRight
|
||||||
|
}
|
||||||
|
if descW <= float64(2*OcellPadX) {
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
lines := pdf.SplitLines([]byte(desc), descW-2*OcellPadX)
|
lines := pdf.SplitLines([]byte(desc), descW-float64(2*OcellPadX))
|
||||||
lineH := 3.2
|
lineH := 3.2
|
||||||
h := float64(len(lines))*lineH + 2
|
h := float64(len(lines))*lineH + 2
|
||||||
|
|
||||||
@@ -1101,7 +1192,7 @@ func drawTotalsBox(
|
|||||||
valueX := x + w - 70 // değerlerin sağda hizalanacağı kolon
|
valueX := x + w - 70 // değerlerin sağda hizalanacağı kolon
|
||||||
|
|
||||||
pdf.SetTextColor(149, 113, 22) // Sol başlık gold
|
pdf.SetTextColor(149, 113, 22) // Sol başlık gold
|
||||||
pdf.SetFont("dejavu-b", "", 8.5)
|
pdf.SetFont("dejavu", "B", 8.5)
|
||||||
|
|
||||||
y := startY + 2
|
y := startY + 2
|
||||||
|
|
||||||
@@ -1112,7 +1203,7 @@ func drawTotalsBox(
|
|||||||
pdf.CellFormat(80, lineH, "TOPLAM TUTAR", "", 0, "L", false, 0, "")
|
pdf.CellFormat(80, lineH, "TOPLAM TUTAR", "", 0, "L", false, 0, "")
|
||||||
|
|
||||||
pdf.SetTextColor(201, 162, 39)
|
pdf.SetTextColor(201, 162, 39)
|
||||||
pdf.SetFont("dejavu-b", "", 9)
|
pdf.SetFont("dejavu", "B", 9)
|
||||||
|
|
||||||
pdf.SetXY(valueX, y)
|
pdf.SetXY(valueX, y)
|
||||||
pdf.CellFormat(65, lineH,
|
pdf.CellFormat(65, lineH,
|
||||||
@@ -1127,7 +1218,7 @@ func drawTotalsBox(
|
|||||||
if hasVat {
|
if hasVat {
|
||||||
|
|
||||||
pdf.SetTextColor(149, 113, 22) // gold başlık
|
pdf.SetTextColor(149, 113, 22) // gold başlık
|
||||||
pdf.SetFont("dejavu-b", "", 8.5)
|
pdf.SetFont("dejavu", "B", 8.5)
|
||||||
|
|
||||||
pdf.SetXY(labelX, y)
|
pdf.SetXY(labelX, y)
|
||||||
pdf.CellFormat(80, lineH,
|
pdf.CellFormat(80, lineH,
|
||||||
@@ -1135,7 +1226,7 @@ func drawTotalsBox(
|
|||||||
"", 0, "L", false, 0, "")
|
"", 0, "L", false, 0, "")
|
||||||
|
|
||||||
pdf.SetTextColor(20, 20, 20)
|
pdf.SetTextColor(20, 20, 20)
|
||||||
pdf.SetFont("dejavu-b", "", 9)
|
pdf.SetFont("dejavu", "B", 9)
|
||||||
|
|
||||||
pdf.SetXY(valueX, y)
|
pdf.SetXY(valueX, y)
|
||||||
pdf.CellFormat(65, lineH,
|
pdf.CellFormat(65, lineH,
|
||||||
@@ -1148,13 +1239,13 @@ func drawTotalsBox(
|
|||||||
3️⃣ KDV DAHİL TOPLAM
|
3️⃣ KDV DAHİL TOPLAM
|
||||||
---------------------------------------------------- */
|
---------------------------------------------------- */
|
||||||
pdf.SetTextColor(201, 162, 39)
|
pdf.SetTextColor(201, 162, 39)
|
||||||
pdf.SetFont("dejavu-b", "", 8.5)
|
pdf.SetFont("dejavu", "B", 8.5)
|
||||||
|
|
||||||
pdf.SetXY(labelX, y)
|
pdf.SetXY(labelX, y)
|
||||||
pdf.CellFormat(80, lineH, "KDV DAHİL TOPLAM TUTAR", "", 0, "L", false, 0, "")
|
pdf.CellFormat(80, lineH, "KDV DAHİL TOPLAM TUTAR", "", 0, "L", false, 0, "")
|
||||||
|
|
||||||
pdf.SetTextColor(20, 20, 20)
|
pdf.SetTextColor(20, 20, 20)
|
||||||
pdf.SetFont("dejavu-b", "", 9)
|
pdf.SetFont("dejavu", "B", 9)
|
||||||
|
|
||||||
pdf.SetXY(valueX, y)
|
pdf.SetXY(valueX, y)
|
||||||
pdf.CellFormat(65, lineH,
|
pdf.CellFormat(65, lineH,
|
||||||
@@ -1183,7 +1274,7 @@ func drawGroupSummaryBar(pdf *gofpdf.Fpdf, layout pdfLayout, groupName string, t
|
|||||||
pdf.SetDrawColor(214, 192, 106)
|
pdf.SetDrawColor(214, 192, 106)
|
||||||
pdf.Rect(x, y, w, h, "DF")
|
pdf.Rect(x, y, w, h, "DF")
|
||||||
|
|
||||||
pdf.SetFont("dejavu-b", "", 8.5)
|
pdf.SetFont("dejavu", "B", 8.5)
|
||||||
pdf.SetTextColor(20, 20, 20)
|
pdf.SetTextColor(20, 20, 20)
|
||||||
|
|
||||||
leftTxt := strings.ToUpper(strings.TrimSpace(groupName))
|
leftTxt := strings.ToUpper(strings.TrimSpace(groupName))
|
||||||
@@ -1327,11 +1418,22 @@ func OrderPDFHandler(db *sql.DB) http.Handler {
|
|||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
orderID := mux.Vars(r)["id"]
|
orderID := mux.Vars(r)["id"]
|
||||||
|
log.Printf("📄 OrderPDFHandler start orderID=%s", orderID)
|
||||||
|
defer func() {
|
||||||
|
if rec := recover(); rec != nil {
|
||||||
|
log.Printf("❌ PANIC OrderPDFHandler orderID=%s: %v", orderID, rec)
|
||||||
|
debug.PrintStack()
|
||||||
|
http.Error(w, fmt.Sprintf("order pdf panic: %v", rec), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if orderID == "" {
|
if orderID == "" {
|
||||||
|
log.Printf("❌ OrderPDFHandler missing order id")
|
||||||
http.Error(w, "missing order id", http.StatusBadRequest)
|
http.Error(w, "missing order id", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if db == nil {
|
if db == nil {
|
||||||
|
log.Printf("❌ OrderPDFHandler db is nil")
|
||||||
http.Error(w, "db not initialized", http.StatusInternalServerError)
|
http.Error(w, "db not initialized", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1339,18 +1441,23 @@ func OrderPDFHandler(db *sql.DB) http.Handler {
|
|||||||
// Header
|
// Header
|
||||||
header, err := getOrderHeaderFromDB(db, orderID)
|
header, err := getOrderHeaderFromDB(db, orderID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("header error:", err)
|
log.Printf("❌ OrderPDF header error orderID=%s: %v", orderID, err)
|
||||||
http.Error(w, "header not found", http.StatusInternalServerError)
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
http.Error(w, "order not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, "header not found: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lines
|
// Lines
|
||||||
lines, err := getOrderLinesFromDB(db, orderID)
|
lines, err := getOrderLinesFromDB(db, orderID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("lines error:", err)
|
log.Printf("❌ OrderPDF lines error orderID=%s: %v", orderID, err)
|
||||||
http.Error(w, "lines not found", http.StatusInternalServerError)
|
http.Error(w, "lines not found: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Printf("📄 OrderPDF lines loaded orderID=%s lineCount=%d", orderID, len(lines))
|
||||||
// 🔹 Satırlardan KDV bilgisi yakala (ilk pozitif orana göre)
|
// 🔹 Satırlardan KDV bilgisi yakala (ilk pozitif orana göre)
|
||||||
hasVat := false
|
hasVat := false
|
||||||
var vatRate float64
|
var vatRate float64
|
||||||
@@ -1365,16 +1472,45 @@ func OrderPDFHandler(db *sql.DB) http.Handler {
|
|||||||
|
|
||||||
// Normalize
|
// Normalize
|
||||||
rows := normalizeOrderLinesForPdf(lines)
|
rows := normalizeOrderLinesForPdf(lines)
|
||||||
|
log.Printf("📄 OrderPDF normalized rows orderID=%s rowCount=%d", orderID, len(rows))
|
||||||
|
for i, rr := range rows {
|
||||||
|
if i >= 30 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
log.Printf(
|
||||||
|
"📄 OrderPDF row[%d] model=%s color=%s groupMain=%q groupSub=%q category=%s totalQty=%d sizeQty=%s",
|
||||||
|
i,
|
||||||
|
rr.Model,
|
||||||
|
rr.Color,
|
||||||
|
rr.GroupMain,
|
||||||
|
rr.GroupSub,
|
||||||
|
rr.Category,
|
||||||
|
rr.TotalQty,
|
||||||
|
formatSizeQtyForLog(rr.SizeQty),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// PDF
|
// PDF
|
||||||
pdf := newOrderPdf()
|
pdf, err := newOrderPdf()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("❌ OrderPDF init error orderID=%s: %v", orderID, err)
|
||||||
|
http.Error(w, "pdf init error: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
renderOrderGrid(pdf, header, rows, hasVat, vatRate)
|
renderOrderGrid(pdf, header, rows, hasVat, vatRate)
|
||||||
|
if err := pdf.Error(); err != nil {
|
||||||
|
log.Printf("❌ OrderPDF render error orderID=%s: %v", orderID, err)
|
||||||
|
http.Error(w, "pdf render error: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := pdf.Output(&buf); err != nil {
|
if err := pdf.Output(&buf); err != nil {
|
||||||
|
log.Printf("❌ OrderPDF output error orderID=%s: %v", orderID, err)
|
||||||
http.Error(w, "pdf output error: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "pdf output error: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Printf("✅ OrderPDF success orderID=%s bytes=%d", orderID, buf.Len())
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/pdf")
|
w.Header().Set("Content-Type", "application/pdf")
|
||||||
w.Header().Set(
|
w.Header().Set(
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ func UpdateOrderHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
"code": "ORDER_UPDATE_FAILED",
|
"code": "ORDER_UPDATE_FAILED",
|
||||||
"message": "Sipariş kaydedilirken beklenmeyen bir hata oluştu.",
|
"message": "Sipariş kaydedilirken beklenmeyen bir hata oluştu.",
|
||||||
|
"detail": err.Error(),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
128
svc/routes/pdf_assets.go
Normal file
128
svc/routes/pdf_assets.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jung-kurt/gofpdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resolvePdfAssetPath(name string) (string, error) {
|
||||||
|
|
||||||
|
base := strings.TrimSpace(os.Getenv("PDF_FONT_DIR"))
|
||||||
|
|
||||||
|
if base == "" {
|
||||||
|
return "", fmt.Errorf("env PDF_FONT_DIR not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(base, "/") {
|
||||||
|
base = "/" + base
|
||||||
|
}
|
||||||
|
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
name = strings.TrimPrefix(name, "/")
|
||||||
|
name = strings.TrimPrefix(name, "\\")
|
||||||
|
|
||||||
|
full := filepath.Join(base, name)
|
||||||
|
full = filepath.Clean(full)
|
||||||
|
|
||||||
|
log.Printf("📄 PDF FONT PATH = %s", full)
|
||||||
|
|
||||||
|
if _, err := os.Stat(full); err != nil {
|
||||||
|
return "", fmt.Errorf("font not found: %s (%v)", full, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return full, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolvePdfImagePath(fileName string) (string, error) {
|
||||||
|
return resolveAssetPath(fileName, []string{
|
||||||
|
"public",
|
||||||
|
filepath.Join("svc", "public"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveAssetPath(fileName string, relativeDirs []string) (string, error) {
|
||||||
|
candidates := make([]string, 0, len(relativeDirs)*3)
|
||||||
|
for _, dir := range relativeDirs {
|
||||||
|
candidates = append(candidates,
|
||||||
|
filepath.Join(dir, fileName),
|
||||||
|
filepath.Join(".", dir, fileName),
|
||||||
|
filepath.Join("..", dir, fileName),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if exePath, err := os.Executable(); err == nil {
|
||||||
|
exeDir := filepath.Dir(exePath)
|
||||||
|
for _, dir := range relativeDirs {
|
||||||
|
candidates = append(candidates,
|
||||||
|
filepath.Join(exeDir, dir, fileName),
|
||||||
|
filepath.Join(exeDir, "..", dir, fileName),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
tried := make([]string, 0, len(candidates))
|
||||||
|
|
||||||
|
for _, p := range candidates {
|
||||||
|
if abs, err := filepath.Abs(p); err == nil {
|
||||||
|
p = abs
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := seen[p]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[p] = struct{}{}
|
||||||
|
tried = append(tried, p)
|
||||||
|
|
||||||
|
if fi, err := os.Stat(p); err == nil && !fi.IsDir() {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("asset not found: %s (tried: %s)", fileName, strings.Join(tried, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerDejavuFonts(pdf *gofpdf.Fpdf, s string) error {
|
||||||
|
|
||||||
|
regPath, err := resolvePdfAssetPath("DejaVuSans.ttf")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
boldPath, err := resolvePdfAssetPath("DejaVuSans-Bold.ttf")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAME FAMILY: "dejavu"
|
||||||
|
pdf.AddUTF8FontFromBytes(
|
||||||
|
"dejavu",
|
||||||
|
"",
|
||||||
|
mustReadFile(regPath),
|
||||||
|
)
|
||||||
|
|
||||||
|
pdf.AddUTF8FontFromBytes(
|
||||||
|
"dejavu",
|
||||||
|
"B",
|
||||||
|
mustReadFile(boldPath),
|
||||||
|
)
|
||||||
|
|
||||||
|
if pdf.Error() != nil {
|
||||||
|
return fmt.Errorf("font init failed: %w", pdf.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustReadFile(path string) []byte {
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
panic("FONT READ ERROR: " + err.Error())
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
// JWT’den 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,
|
||||||
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"runtime/debug"
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -53,9 +52,7 @@ var hMainWbase = []float64{
|
|||||||
// Font dosyaları
|
// Font dosyaları
|
||||||
const (
|
const (
|
||||||
hFontFamilyReg = "dejavu"
|
hFontFamilyReg = "dejavu"
|
||||||
hFontFamilyBold = "dejavu-b"
|
hFontFamilyBold = "dejavu"
|
||||||
hFontPathReg = "fonts/DejaVuSans.ttf"
|
|
||||||
hFontPathBold = "fonts/DejaVuSans-Bold.ttf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Renkler
|
// Renkler
|
||||||
@@ -66,13 +63,8 @@ var (
|
|||||||
|
|
||||||
/* ============================ FONT / FORMAT ============================ */
|
/* ============================ FONT / FORMAT ============================ */
|
||||||
|
|
||||||
func hEnsureFonts(pdf *gofpdf.Fpdf) {
|
func hEnsureFonts(pdf *gofpdf.Fpdf) error {
|
||||||
if _, err := os.Stat(hFontPathReg); err == nil {
|
return registerDejavuFonts(pdf, hFontFamilyReg)
|
||||||
pdf.AddUTF8Font(hFontFamilyReg, "", hFontPathReg)
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(hFontPathBold); err == nil {
|
|
||||||
pdf.AddUTF8Font(hFontFamilyBold, "", hFontPathBold)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func hNormalizeWidths(base []float64, targetTotal float64) []float64 {
|
func hNormalizeWidths(base []float64, targetTotal float64) []float64 {
|
||||||
@@ -145,10 +137,9 @@ func hCalcRowHeightForText(pdf *gofpdf.Fpdf, text string, colWidth, lineHeight,
|
|||||||
/* ============================ HEADER ============================ */
|
/* ============================ HEADER ============================ */
|
||||||
|
|
||||||
func hDrawPageHeader(pdf *gofpdf.Fpdf, cariKod, cariIsim, start, end string) float64 {
|
func hDrawPageHeader(pdf *gofpdf.Fpdf, cariKod, cariIsim, start, end string) float64 {
|
||||||
logoPath, _ := filepath.Abs("./public/Baggi-Tekstil-A.s-Logolu.jpeg")
|
if logoPath, err := resolvePdfImagePath("Baggi-Tekstil-A.s-Logolu.jpeg"); err == nil {
|
||||||
|
|
||||||
// Logo
|
|
||||||
pdf.ImageOptions(logoPath, hMarginL, 2, hLogoW, 0, false, gofpdf.ImageOptions{}, 0, "")
|
pdf.ImageOptions(logoPath, hMarginL, 2, hLogoW, 0, false, gofpdf.ImageOptions{}, 0, "")
|
||||||
|
}
|
||||||
|
|
||||||
// Başlıklar
|
// Başlıklar
|
||||||
pdf.SetFont(hFontFamilyBold, "", 16)
|
pdf.SetFont(hFontFamilyBold, "", 16)
|
||||||
@@ -272,6 +263,14 @@ func hDrawMainDataRow(pdf *gofpdf.Fpdf, row []string, widths []float64, rowH flo
|
|||||||
|
|
||||||
func ExportStatementHeaderReportPDFHandler(mssql *sql.DB) http.HandlerFunc {
|
func ExportStatementHeaderReportPDFHandler(mssql *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer func() {
|
||||||
|
if rec := recover(); rec != nil {
|
||||||
|
log.Printf("❌ PANIC ExportStatementHeaderReportPDFHandler: %v", rec)
|
||||||
|
debug.PrintStack()
|
||||||
|
http.Error(w, fmt.Sprintf("header PDF panic: %v", rec), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
claims, ok := auth.GetClaimsFromContext(r.Context())
|
claims, ok := auth.GetClaimsFromContext(r.Context())
|
||||||
if !ok || claims == nil {
|
if !ok || claims == nil {
|
||||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
@@ -332,7 +331,10 @@ func ExportStatementHeaderReportPDFHandler(mssql *sql.DB) http.HandlerFunc {
|
|||||||
pdf := gofpdf.New("L", "mm", "A4", "")
|
pdf := gofpdf.New("L", "mm", "A4", "")
|
||||||
pdf.SetMargins(hMarginL, hMarginT, hMarginR)
|
pdf.SetMargins(hMarginL, hMarginT, hMarginR)
|
||||||
pdf.SetAutoPageBreak(false, hMarginB)
|
pdf.SetAutoPageBreak(false, hMarginB)
|
||||||
hEnsureFonts(pdf)
|
if err := hEnsureFonts(pdf); err != nil {
|
||||||
|
http.Error(w, "PDF font yükleme hatası: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
wAvail := hPageWidth - hMarginL - hMarginR
|
wAvail := hPageWidth - hMarginL - hMarginR
|
||||||
mainWn := hNormalizeWidths(hMainWbase, wAvail)
|
mainWn := hNormalizeWidths(hMainWbase, wAvail)
|
||||||
@@ -380,6 +382,11 @@ func ExportStatementHeaderReportPDFHandler(mssql *sql.DB) http.HandlerFunc {
|
|||||||
pdf.Ln(1)
|
pdf.Ln(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := pdf.Error(); err != nil {
|
||||||
|
http.Error(w, "PDF render hatası: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := pdf.Output(&buf); err != nil {
|
if err := pdf.Output(&buf); err != nil {
|
||||||
http.Error(w, "PDF oluşturulamadı: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "PDF oluşturulamadı: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"runtime/debug"
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -79,9 +78,7 @@ var dWbase = []float64{
|
|||||||
// Font dosyaları
|
// Font dosyaları
|
||||||
const (
|
const (
|
||||||
fontFamilyReg = "dejavu"
|
fontFamilyReg = "dejavu"
|
||||||
fontFamilyBold = "dejavu-b"
|
fontFamilyBold = "dejavu"
|
||||||
fontPathReg = "fonts/DejaVuSans.ttf"
|
|
||||||
fontPathBold = "fonts/DejaVuSans-Bold.ttf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Kurumsal renkler
|
// Kurumsal renkler
|
||||||
@@ -134,17 +131,8 @@ func formatCurrencyTR(n float64) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fontları yükle
|
// Fontları yükle
|
||||||
func ensureFonts(pdf *gofpdf.Fpdf) {
|
func ensureFonts(pdf *gofpdf.Fpdf) error {
|
||||||
if _, err := os.Stat(fontPathReg); err == nil {
|
return registerDejavuFonts(pdf, fontFamilyReg)
|
||||||
pdf.AddUTF8Font(fontFamilyReg, "", fontPathReg)
|
|
||||||
} else {
|
|
||||||
log.Printf("⚠️ Font bulunamadı: %s", fontPathReg)
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(fontPathBold); err == nil {
|
|
||||||
pdf.AddUTF8Font(fontFamilyBold, "", fontPathBold)
|
|
||||||
} else {
|
|
||||||
log.Printf("⚠️ Font bulunamadı: %s", fontPathBold)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Güvenli satır kırma
|
// Güvenli satır kırma
|
||||||
@@ -237,10 +225,9 @@ func drawLabeledBox(pdf *gofpdf.Fpdf, x, y, w, h float64, label, value string, a
|
|||||||
}
|
}
|
||||||
|
|
||||||
func drawPageHeader(pdf *gofpdf.Fpdf, cariKod, cariIsim, start, end string) float64 {
|
func drawPageHeader(pdf *gofpdf.Fpdf, cariKod, cariIsim, start, end string) float64 {
|
||||||
logoPath, _ := filepath.Abs("./public/Baggi-Tekstil-A.s-Logolu.jpeg")
|
if logoPath, err := resolvePdfImagePath("Baggi-Tekstil-A.s-Logolu.jpeg"); err == nil {
|
||||||
|
|
||||||
// Logo
|
|
||||||
pdf.ImageOptions(logoPath, hMarginL, 2, hLogoW, 0, false, gofpdf.ImageOptions{}, 0, "")
|
pdf.ImageOptions(logoPath, hMarginL, 2, hLogoW, 0, false, gofpdf.ImageOptions{}, 0, "")
|
||||||
|
}
|
||||||
|
|
||||||
// Başlıklar
|
// Başlıklar
|
||||||
pdf.SetFont(hFontFamilyBold, "", 16)
|
pdf.SetFont(hFontFamilyBold, "", 16)
|
||||||
@@ -435,7 +422,8 @@ func ExportPDFHandler(mssql *sql.DB) http.HandlerFunc {
|
|||||||
defer func() {
|
defer func() {
|
||||||
if rec := recover(); rec != nil {
|
if rec := recover(); rec != nil {
|
||||||
log.Printf("❌ PANIC ExportPDFHandler: %v", rec)
|
log.Printf("❌ PANIC ExportPDFHandler: %v", rec)
|
||||||
http.Error(w, "PDF oluşturulurken hata oluştu", http.StatusInternalServerError)
|
debug.PrintStack()
|
||||||
|
http.Error(w, fmt.Sprintf("PDF oluşturulurken panic oluştu: %v", rec), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -513,7 +501,10 @@ func ExportPDFHandler(mssql *sql.DB) http.HandlerFunc {
|
|||||||
pdf := gofpdf.New("L", "mm", "A4", "")
|
pdf := gofpdf.New("L", "mm", "A4", "")
|
||||||
pdf.SetMargins(marginL, marginT, marginR)
|
pdf.SetMargins(marginL, marginT, marginR)
|
||||||
pdf.SetAutoPageBreak(false, marginB)
|
pdf.SetAutoPageBreak(false, marginB)
|
||||||
ensureFonts(pdf)
|
if err := ensureFonts(pdf); err != nil {
|
||||||
|
http.Error(w, "PDF font yükleme hatası: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
pdf.SetFont(fontFamilyReg, "", 8.5)
|
pdf.SetFont(fontFamilyReg, "", 8.5)
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
|
|
||||||
@@ -614,6 +605,11 @@ func ExportPDFHandler(mssql *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 7) Çıktı
|
// 7) Çıktı
|
||||||
|
if err := pdf.Error(); err != nil {
|
||||||
|
http.Error(w, "PDF render hatası: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := pdf.Output(&buf); err != nil {
|
if err := pdf.Output(&buf); err != nil {
|
||||||
http.Error(w, "PDF oluşturulamadı: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "PDF oluşturulamadı: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bssapp-backend/auth"
|
||||||
"bssapp-backend/internal/auditlog"
|
"bssapp-backend/internal/auditlog"
|
||||||
"bssapp-backend/internal/mailer"
|
"bssapp-backend/internal/mailer"
|
||||||
"bssapp-backend/internal/security"
|
"bssapp-backend/internal/security"
|
||||||
@@ -11,9 +12,11 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@@ -49,6 +52,8 @@ func UserDetailRoute(db *sql.DB) http.Handler {
|
|||||||
handleUserGet(db, w, userID)
|
handleUserGet(db, w, userID)
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
handleUserUpdate(db, w, r, userID)
|
handleUserUpdate(db, w, r, userID)
|
||||||
|
case http.MethodDelete:
|
||||||
|
handleUserDelete(db, w, r, userID)
|
||||||
case http.MethodOptions:
|
case http.MethodOptions:
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
default:
|
default:
|
||||||
@@ -210,6 +215,17 @@ func handleUserUpdate(db *sql.DB, w http.ResponseWriter, r *http.Request, userID
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
payload.Code = strings.TrimSpace(payload.Code)
|
||||||
|
payload.FullName = strings.TrimSpace(payload.FullName)
|
||||||
|
payload.Email = strings.TrimSpace(payload.Email)
|
||||||
|
payload.Mobile = strings.TrimSpace(payload.Mobile)
|
||||||
|
payload.Address = strings.TrimSpace(payload.Address)
|
||||||
|
|
||||||
|
if payload.Code == "" {
|
||||||
|
http.Error(w, "Kullanıcı kodu zorunludur", http.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
tx, err := db.Begin()
|
tx, err := db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError)
|
http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError)
|
||||||
@@ -228,31 +244,81 @@ func handleUserUpdate(db *sql.DB, w http.ResponseWriter, r *http.Request, userID
|
|||||||
payload.Address,
|
payload.Address,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("❌ [UserDetail] UpdateUserHeader failed user_id=%d err=%v payload=%+v", userID, err, payload)
|
||||||
http.Error(w, "Header güncellenemedi", http.StatusInternalServerError)
|
http.Error(w, "Header güncellenemedi", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.Exec(`DELETE FROM dfrole_usr WHERE dfusr_id = $1`, userID)
|
if _, err := tx.Exec(`DELETE FROM dfrole_usr WHERE dfusr_id = $1`, userID); err != nil {
|
||||||
|
log.Printf("❌ [UserDetail] delete roles failed user_id=%d err=%v", userID, err)
|
||||||
|
http.Error(w, "Roller temizlenemedi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
for _, code := range payload.Roles {
|
for _, code := range payload.Roles {
|
||||||
tx.Exec(queries.InsertUserRole, userID, code)
|
code = strings.TrimSpace(code)
|
||||||
|
if code == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(queries.InsertUserRole, userID, code); err != nil {
|
||||||
|
log.Printf("❌ [UserDetail] insert role failed user_id=%d role=%q err=%v", userID, code, err)
|
||||||
|
http.Error(w, "Rol eklenemedi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.Exec(`DELETE FROM dfusr_dprt WHERE dfusr_id = $1`, userID)
|
if _, err := tx.Exec(`DELETE FROM dfusr_dprt WHERE dfusr_id = $1`, userID); err != nil {
|
||||||
|
log.Printf("❌ [UserDetail] delete departments failed user_id=%d err=%v", userID, err)
|
||||||
|
http.Error(w, "Departmanlar temizlenemedi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
for _, d := range payload.Departments {
|
for _, d := range payload.Departments {
|
||||||
tx.Exec(queries.InsertUserDepartment, userID, d.Code)
|
code := strings.TrimSpace(d.Code)
|
||||||
|
if code == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(queries.InsertUserDepartment, userID, code); err != nil {
|
||||||
|
log.Printf("❌ [UserDetail] insert department failed user_id=%d dept=%q err=%v", userID, code, err)
|
||||||
|
http.Error(w, "Departman eklenemedi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.Exec(`DELETE FROM dfusr_piyasa WHERE dfusr_id = $1`, userID)
|
if _, err := tx.Exec(`DELETE FROM dfusr_piyasa WHERE dfusr_id = $1`, userID); err != nil {
|
||||||
|
log.Printf("❌ [UserDetail] delete piyasalar failed user_id=%d err=%v", userID, err)
|
||||||
|
http.Error(w, "Piyasalar temizlenemedi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
for _, p := range payload.Piyasalar {
|
for _, p := range payload.Piyasalar {
|
||||||
tx.Exec(queries.InsertUserPiyasa, userID, p.Code)
|
code := strings.TrimSpace(p.Code)
|
||||||
|
if code == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(queries.InsertUserPiyasa, userID, code); err != nil {
|
||||||
|
log.Printf("❌ [UserDetail] insert piyasa failed user_id=%d piyasa=%q err=%v", userID, code, err)
|
||||||
|
http.Error(w, "Piyasa eklenemedi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.Exec(`DELETE FROM dfusr_nebim_user WHERE dfusr_id = $1`, userID)
|
if _, err := tx.Exec(`DELETE FROM dfusr_nebim_user WHERE dfusr_id = $1`, userID); err != nil {
|
||||||
|
log.Printf("❌ [UserDetail] delete nebim users failed user_id=%d err=%v", userID, err)
|
||||||
|
http.Error(w, "Nebim kullanıcıları temizlenemedi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
for _, n := range payload.NebimUsers {
|
for _, n := range payload.NebimUsers {
|
||||||
tx.Exec(queries.InsertUserNebim, userID, n.Username)
|
username := strings.TrimSpace(n.Username)
|
||||||
|
if username == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(queries.InsertUserNebim, userID, username); err != nil {
|
||||||
|
log.Printf("❌ [UserDetail] insert nebim user failed user_id=%d username=%q err=%v", userID, username, err)
|
||||||
|
http.Error(w, "Nebim kullanıcısı eklenemedi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
|
log.Printf("❌ [UserDetail] commit failed user_id=%d err=%v", userID, err)
|
||||||
http.Error(w, "Commit başarısız", http.StatusInternalServerError)
|
http.Error(w, "Commit başarısız", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -260,6 +326,102 @@ func handleUserUpdate(db *sql.DB, w http.ResponseWriter, r *http.Request, userID
|
|||||||
_ = json.NewEncoder(w).Encode(map[string]any{"success": true})
|
_ = json.NewEncoder(w).Encode(map[string]any{"success": true})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 🗑️ DELETE USER (HARD DELETE)
|
||||||
|
// ======================================================
|
||||||
|
func handleUserDelete(db *sql.DB, w http.ResponseWriter, r *http.Request, userID int64) {
|
||||||
|
claims, _ := auth.GetClaimsFromContext(r.Context())
|
||||||
|
if claims != nil && int64(claims.ID) == userID {
|
||||||
|
http.Error(w, "Kendi kullanicinizi silemezsiniz", http.StatusConflict)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Transaction baslatilamadi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
var username string
|
||||||
|
_ = tx.QueryRow(`
|
||||||
|
SELECT username
|
||||||
|
FROM mk_dfusr
|
||||||
|
WHERE id = $1
|
||||||
|
`, userID).Scan(&username)
|
||||||
|
|
||||||
|
if strings.TrimSpace(username) == "" {
|
||||||
|
_ = tx.QueryRow(`
|
||||||
|
SELECT code
|
||||||
|
FROM dfusr
|
||||||
|
WHERE id = $1
|
||||||
|
`, userID).Scan(&username)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(username) == "" {
|
||||||
|
http.Error(w, "Kullanici bulunamadi", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupQueries := []string{
|
||||||
|
`DELETE FROM mk_refresh_tokens WHERE mk_user_id = $1`,
|
||||||
|
`DELETE FROM mk_dfusr_password_reset WHERE mk_dfusr_id = $1`,
|
||||||
|
`DELETE FROM dfusr_password_reset WHERE dfusr_id = $1`,
|
||||||
|
`DELETE FROM mk_sys_user_permissions WHERE user_id = $1`,
|
||||||
|
`DELETE FROM dfrole_usr WHERE dfusr_id = $1`,
|
||||||
|
`DELETE FROM dfusr_dprt WHERE dfusr_id = $1`,
|
||||||
|
`DELETE FROM dfusr_piyasa WHERE dfusr_id = $1`,
|
||||||
|
`DELETE FROM dfusr_nebim_user WHERE dfusr_id = $1`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, q := range cleanupQueries {
|
||||||
|
if _, err := tx.Exec(q, userID); err != nil {
|
||||||
|
log.Printf("❌ [UserDetail] cleanup failed user_id=%d err=%v query=%s", userID, err, q)
|
||||||
|
http.Error(w, "Kullanici baglantilari silinemedi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := tx.Exec(`DELETE FROM mk_dfusr WHERE id = $1`, userID); err != nil {
|
||||||
|
log.Printf("❌ [UserDetail] delete mk_dfusr failed user_id=%d err=%v", userID, err)
|
||||||
|
http.Error(w, "Kullanici silinemedi", http.StatusInternalServerError)
|
||||||
|
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 {
|
||||||
|
log.Printf("❌ [UserDetail] delete commit failed user_id=%d err=%v", userID, err)
|
||||||
|
http.Error(w, "Commit basarisiz", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims != nil {
|
||||||
|
auditlog.Enqueue(r.Context(), auditlog.ActivityLog{
|
||||||
|
ActionType: "user_delete",
|
||||||
|
ActionCategory: "user_admin",
|
||||||
|
ActionTarget: fmt.Sprintf("/api/users/%d", userID),
|
||||||
|
Description: "user deleted from mk_dfusr and dfusr",
|
||||||
|
Username: claims.Username,
|
||||||
|
RoleCode: claims.RoleCode,
|
||||||
|
DfUsrID: int64(claims.ID),
|
||||||
|
TargetDfUsrID: userID,
|
||||||
|
TargetUsername: username,
|
||||||
|
IsSuccess: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"success": true,
|
||||||
|
"deleted": userID,
|
||||||
|
"username": username,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ======================================================
|
// ======================================================
|
||||||
// 🔐 ADMIN — PASSWORD RESET MAIL
|
// 🔐 ADMIN — PASSWORD RESET MAIL
|
||||||
// ======================================================
|
// ======================================================
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
VITE_API_BASE_URL=http://localhost:8080
|
VITE_API_BASE_URL=http://localhost:8080/api
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
10
ui/package-lock.json
generated
10
ui/package-lock.json
generated
@@ -7,7 +7,6 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "baggisowtfaresystem",
|
"name": "baggisowtfaresystem",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"hasInstallScript": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@quasar/extras": "^1.16.4",
|
"@quasar/extras": "^1.16.4",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
@@ -20,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",
|
||||||
@@ -3856,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": {
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"No test specified\" && exit 0",
|
"test": "echo \"No test specified\" && exit 0",
|
||||||
"dev": "quasar dev",
|
"dev": "quasar dev",
|
||||||
"build": "quasar build",
|
"build": "quasar build"
|
||||||
"postinstall": "quasar prepare"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@quasar/extras": "^1.16.4",
|
"@quasar/extras": "^1.16.4",
|
||||||
@@ -24,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",
|
||||||
|
|||||||
@@ -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 (CORS’suz)
|
// 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(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -226,25 +226,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 +258,7 @@ const menuItems = [
|
|||||||
{
|
{
|
||||||
label: 'Kullanıcılar',
|
label: 'Kullanıcılar',
|
||||||
to: '/app/users',
|
to: '/app/users',
|
||||||
permission: 'user:view'
|
permission: 'system:read'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ Mevcut şifrenizi girerek yeni şifre belirleyin
|
|||||||
|
|
||||||
<q-separator />
|
<q-separator />
|
||||||
|
|
||||||
|
<q-form @submit.prevent="submit">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="current"
|
v-model="current"
|
||||||
@@ -46,13 +47,14 @@ class="bg-red-1 text-red q-mt-md"
|
|||||||
<q-card-actions align="right">
|
<q-card-actions align="right">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="canUpdateSystem"
|
v-if="canUpdateSystem"
|
||||||
|
type="submit"
|
||||||
label="GÜNCELLE"
|
label="GÜNCELLE"
|
||||||
color="primary"
|
color="primary"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:disable="!canSubmit"
|
:disable="!canSubmit"
|
||||||
@click="submit"
|
|
||||||
/>
|
/>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
|
</q-form>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
</q-page>
|
</q-page>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-form @submit.prevent="submit">
|
||||||
<q-card-section class="q-gutter-md">
|
<q-card-section class="q-gutter-md">
|
||||||
<q-input
|
<q-input
|
||||||
v-model="currentPassword"
|
v-model="currentPassword"
|
||||||
@@ -44,12 +45,13 @@
|
|||||||
|
|
||||||
<q-card-actions align="right">
|
<q-card-actions align="right">
|
||||||
<q-btn
|
<q-btn
|
||||||
|
type="submit"
|
||||||
label="Kaydet"
|
label="Kaydet"
|
||||||
color="primary"
|
color="primary"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@click="submit"
|
|
||||||
/>
|
/>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
|
</q-form>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
@@ -57,7 +59,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import api from 'src/services/api'
|
import api, { extractApiErrorDetail } from 'src/services/api'
|
||||||
import { useAuthStore } from 'stores/authStore.js'
|
import { useAuthStore } from 'stores/authStore.js'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -69,6 +71,33 @@ const newPassword2 = ref('')
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
|
||||||
|
function resolveFirstPasswordError(status, detail) {
|
||||||
|
const text = String(detail || '').trim()
|
||||||
|
const lower = text.toLowerCase()
|
||||||
|
|
||||||
|
if (status === 401) {
|
||||||
|
if (lower.includes('mevcut sifre') || lower.includes('current password')) {
|
||||||
|
return 'Mevcut şifreyi yanlış girdiniz.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.includes('token') || lower.includes('authorization')) {
|
||||||
|
return 'Oturum doğrulanamadı. Lütfen tekrar giriş yapın.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return text || 'Kimlik doğrulama hatası (401). Lütfen tekrar giriş yapın.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 403) {
|
||||||
|
if (lower.includes('permission')) {
|
||||||
|
return 'Şifre değiştirme yetkiniz yok (403). Sistem yöneticinize başvurun.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return text || 'Bu işlem için yetkiniz yok (403).'
|
||||||
|
}
|
||||||
|
|
||||||
|
return text || 'Şifre güncellenemedi'
|
||||||
|
}
|
||||||
|
|
||||||
async function submit () {
|
async function submit () {
|
||||||
error.value = ''
|
error.value = ''
|
||||||
|
|
||||||
@@ -84,29 +113,25 @@ async function submit () {
|
|||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
// 🔐 TOKEN interceptor ile otomatik
|
await api.post('/password/change', {
|
||||||
const res = await api.post('/password/change', {
|
|
||||||
current_password: currentPassword.value,
|
current_password: currentPassword.value,
|
||||||
new_password: newPassword.value
|
new_password: newPassword.value
|
||||||
})
|
})
|
||||||
|
|
||||||
// 🔄 Session güncelle
|
auth.clearSession()
|
||||||
auth.setSession(res.data)
|
router.replace('/login')
|
||||||
auth.forcePasswordChange = false
|
|
||||||
localStorage.setItem('forcePasswordChange', '0')
|
|
||||||
|
|
||||||
router.replace('/app')
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value =
|
const status = e?.response?.status
|
||||||
e?.data?.message ||
|
const detail = await extractApiErrorDetail(e)
|
||||||
e?.message ||
|
|
||||||
'Şifre güncellenemedi'
|
console.error('FIRST_PASSWORD_CHANGE failed', {
|
||||||
|
status,
|
||||||
|
detail
|
||||||
|
})
|
||||||
|
|
||||||
|
error.value = resolveFirstPasswordError(status, detail)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<div class="login-title q-mt-sm">Kullanıcı Girişi</div>
|
<div class="login-title q-mt-sm">Kullanıcı Girişi</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-form @submit.prevent="login">
|
||||||
<!-- FORM -->
|
<!-- FORM -->
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-input
|
<q-input
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
<!-- ACTION -->
|
<!-- ACTION -->
|
||||||
<q-card-actions align="center">
|
<q-card-actions align="center">
|
||||||
<q-btn
|
<q-btn
|
||||||
|
type="submit"
|
||||||
label="Giriş Yap"
|
label="Giriş Yap"
|
||||||
color="primary"
|
color="primary"
|
||||||
glossy
|
glossy
|
||||||
@@ -67,9 +69,9 @@
|
|||||||
icon="login"
|
icon="login"
|
||||||
class="full-width"
|
class="full-width"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@click="login"
|
|
||||||
/>
|
/>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
|
</q-form>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
<!-- 🔐 FORGOT PASSWORD -->
|
<!-- 🔐 FORGOT PASSWORD -->
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
<q-separator />
|
<q-separator />
|
||||||
|
|
||||||
|
<q-form @submit.prevent="submit">
|
||||||
<q-card-section class="q-gutter-sm">
|
<q-card-section class="q-gutter-sm">
|
||||||
<q-input
|
<q-input
|
||||||
v-model="current"
|
v-model="current"
|
||||||
@@ -44,13 +45,14 @@
|
|||||||
|
|
||||||
<q-card-actions align="right">
|
<q-card-actions align="right">
|
||||||
<q-btn
|
<q-btn
|
||||||
|
type="submit"
|
||||||
label="GÜNCELLE"
|
label="GÜNCELLE"
|
||||||
color="primary"
|
color="primary"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:disable="!canSubmit"
|
:disable="!canSubmit"
|
||||||
@click="submit"
|
|
||||||
/>
|
/>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
|
</q-form>
|
||||||
|
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-page>
|
</q-page>
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ import { useQuasar } from 'quasar'
|
|||||||
import { useOrderListStore } from 'src/stores/OrdernewListStore'
|
import { useOrderListStore } from 'src/stores/OrdernewListStore'
|
||||||
import { useAuthStore } from 'src/stores/authStore'
|
import { useAuthStore } from 'src/stores/authStore'
|
||||||
import { usePermission } from 'src/composables/usePermission'
|
import { usePermission } from 'src/composables/usePermission'
|
||||||
import api from 'src/services/api'
|
import api, { extractApiErrorDetail } from 'src/services/api'
|
||||||
|
|
||||||
const { canRead } = usePermission()
|
const { canRead } = usePermission()
|
||||||
const canReadOrder = canRead('order')
|
const canReadOrder = canRead('order')
|
||||||
@@ -392,8 +392,11 @@ async function printPDF (row) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
window.open(URL.createObjectURL(res.data), '_blank')
|
window.open(URL.createObjectURL(res.data), '_blank')
|
||||||
} catch {
|
} catch (err) {
|
||||||
$q.notify({ type: 'negative', message: 'PDF yüklenemedi' })
|
const detail = await extractApiErrorDetail(err)
|
||||||
|
const status = err?.status || err?.response?.status || '-'
|
||||||
|
console.error(`PDF load error [${status}] /order/pdf/${row.OrderHeaderID}: ${detail}`)
|
||||||
|
$q.notify({ type: 'negative', message: `PDF yuklenemedi (${status}): ${detail}` })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function clearFilters () {
|
function clearFilters () {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
<q-separator />
|
<q-separator />
|
||||||
|
|
||||||
|
<q-form @submit.prevent="submit">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<!-- NEW PASSWORD -->
|
<!-- NEW PASSWORD -->
|
||||||
<q-input
|
<q-input
|
||||||
@@ -84,13 +85,14 @@
|
|||||||
|
|
||||||
<q-card-actions align="right">
|
<q-card-actions align="right">
|
||||||
<q-btn
|
<q-btn
|
||||||
|
type="submit"
|
||||||
label="PAROLAYI GÜNCELLE"
|
label="PAROLAYI GÜNCELLE"
|
||||||
color="primary"
|
color="primary"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:disable="!canSubmit"
|
:disable="!canSubmit"
|
||||||
@click="submit"
|
|
||||||
/>
|
/>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
|
</q-form>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
<!-- ❌ TOKEN INVALID -->
|
<!-- ❌ TOKEN INVALID -->
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<div class="filter-bar row q-col-gutter-md q-mb-sm">
|
<div class="filter-bar row q-col-gutter-md q-mb-sm">
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
<div class="text-caption text-grey-7 q-mb-xs">Kullanıcı Kodu</div>
|
<div class="text-caption text-grey-7 q-mb-xs">Kullanıcı Kodu</div>
|
||||||
<q-input v-model="form.code" dense filled />
|
<q-input v-model="form.code" dense filled :rules="[codeRule]" lazy-rules />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
@@ -46,6 +46,15 @@
|
|||||||
:loading="saving"
|
:loading="saving"
|
||||||
@click="onSave"
|
@click="onSave"
|
||||||
/>
|
/>
|
||||||
|
<q-btn
|
||||||
|
v-if="canDeleteThisUser"
|
||||||
|
label="SIL"
|
||||||
|
color="negative"
|
||||||
|
icon="delete"
|
||||||
|
class="q-ml-sm"
|
||||||
|
:loading="deleting"
|
||||||
|
@click="confirmDeleteUser"
|
||||||
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="canReadUser"
|
v-if="canReadUser"
|
||||||
label="LİSTEYE DÖN"
|
label="LİSTEYE DÖN"
|
||||||
@@ -156,12 +165,22 @@
|
|||||||
filled
|
filled
|
||||||
behavior="menu"
|
behavior="menu"
|
||||||
>
|
>
|
||||||
|
<template #selected-item="scope">
|
||||||
|
<q-chip
|
||||||
|
removable
|
||||||
|
dense
|
||||||
|
class="q-mr-xs"
|
||||||
|
@remove="scope.removeAtIndex(scope.index)"
|
||||||
|
>
|
||||||
|
{{ scope.opt.label }}
|
||||||
|
</q-chip>
|
||||||
|
</template>
|
||||||
<template #option="scope">
|
<template #option="scope">
|
||||||
<q-item v-bind="scope.itemProps" clickable>
|
<q-item v-bind="scope.itemProps">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
:model-value="scope.selected"
|
:model-value="scope.selected"
|
||||||
@update:model-value="scope.toggleOption(scope.opt)"
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
@@ -205,12 +224,33 @@
|
|||||||
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">
|
||||||
|
<q-chip
|
||||||
|
removable
|
||||||
|
dense
|
||||||
|
class="q-mr-xs"
|
||||||
|
@remove="scope.removeAtIndex(scope.index)"
|
||||||
|
>
|
||||||
|
{{ scope.opt.label }}
|
||||||
|
</q-chip>
|
||||||
|
</template>
|
||||||
<template #option="scope">
|
<template #option="scope">
|
||||||
<q-item v-bind="scope.itemProps" clickable>
|
<q-item v-bind="scope.itemProps">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
:model-value="scope.selected"
|
:model-value="scope.selected"
|
||||||
@update:model-value="scope.toggleOption(scope.opt)"
|
tabindex="-1"
|
||||||
|
@update:model-value="() => scope.toggleOption(scope.opt)"
|
||||||
|
@click.stop
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
@@ -259,10 +299,11 @@ import { storeToRefs } from 'pinia'
|
|||||||
import { useUserDetailStore } from 'src/stores/UserDetailStore'
|
import { useUserDetailStore } from 'src/stores/UserDetailStore'
|
||||||
import { usePermission } from 'src/composables/usePermission'
|
import { usePermission } from 'src/composables/usePermission'
|
||||||
|
|
||||||
const { canRead, canWrite, canUpdate } = usePermission()
|
const { canRead, canWrite, canUpdate, canDelete } = usePermission()
|
||||||
const canReadUser = canRead('user')
|
const canReadUser = canRead('user')
|
||||||
const canWriteUser = canWrite('user')
|
const canWriteUser = canWrite('user')
|
||||||
const canUpdateUser = canUpdate('user')
|
const canUpdateUser = canUpdate('user')
|
||||||
|
const canDeleteUser = canDelete('user')
|
||||||
|
|
||||||
const $q = useQuasar()
|
const $q = useQuasar()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -274,6 +315,7 @@ const {
|
|||||||
form,
|
form,
|
||||||
loading,
|
loading,
|
||||||
saving,
|
saving,
|
||||||
|
deleting,
|
||||||
roleOptions,
|
roleOptions,
|
||||||
departmentOptions,
|
departmentOptions,
|
||||||
piyasaOptions,
|
piyasaOptions,
|
||||||
@@ -296,6 +338,11 @@ const canAccessPage = computed(() => {
|
|||||||
const canSaveUser = computed(() => isNew.value ? canWriteUser.value : canUpdateUser.value)
|
const canSaveUser = computed(() => isNew.value ? canWriteUser.value : canUpdateUser.value)
|
||||||
|
|
||||||
const userId = computed(() => (isEdit.value || isView.value) ? Number(route.params.id) : null)
|
const userId = computed(() => (isEdit.value || isView.value) ? Number(route.params.id) : null)
|
||||||
|
const canDeleteThisUser = computed(() =>
|
||||||
|
(isEdit.value || isView.value) &&
|
||||||
|
!!userId.value &&
|
||||||
|
canDeleteUser.value
|
||||||
|
)
|
||||||
|
|
||||||
const hasPassword = computed(() => store.hasPassword)
|
const hasPassword = computed(() => store.hasPassword)
|
||||||
|
|
||||||
@@ -316,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,
|
||||||
@@ -341,6 +398,11 @@ async function onSave () {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(form.value.code || '').trim()) {
|
||||||
|
$q.notify({ type: 'negative', message: 'Kullanıcı kodu zorunludur' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('🟢 onSave() START', { mode: mode.value })
|
console.log('🟢 onSave() START', { mode: mode.value })
|
||||||
|
|
||||||
@@ -381,6 +443,36 @@ function goList () {
|
|||||||
router.push({ name: 'user-list' })
|
router.push({ name: 'user-list' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function confirmDeleteUser () {
|
||||||
|
if (!canDeleteThisUser.value) {
|
||||||
|
$q.notify({ type: 'negative', message: 'Silme yetkiniz yok' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = (form.value.code || '').trim()
|
||||||
|
const label = code || `ID ${userId.value}`
|
||||||
|
|
||||||
|
$q.dialog({
|
||||||
|
title: 'Kullanici silinsin mi?',
|
||||||
|
message: `${label} kaydi tablodan silinecek. Bu islem geri alinamaz.`,
|
||||||
|
cancel: true,
|
||||||
|
persistent: true
|
||||||
|
}).onOk(async () => {
|
||||||
|
await deleteUser()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteUser () {
|
||||||
|
try {
|
||||||
|
await store.deleteUser(userId.value)
|
||||||
|
$q.notify({ type: 'positive', message: 'Kullanici silindi' })
|
||||||
|
await router.replace({ name: 'user-list' })
|
||||||
|
window.location.reload()
|
||||||
|
} catch {
|
||||||
|
$q.notify({ type: 'negative', message: store.error || 'Kullanici silinemedi' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function confirmSendPasswordMail () {
|
function confirmSendPasswordMail () {
|
||||||
$q.dialog({
|
$q.dialog({
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 ================= */
|
||||||
|
|
||||||
|
|||||||
@@ -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' }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
// src/services/api.js
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import qs from 'qs'
|
import qs from 'qs'
|
||||||
import { useAuthStore } from 'stores/authStore'
|
import { useAuthStore } from 'stores/authStore'
|
||||||
|
|
||||||
// ✅ Vite uyumlu env okuma
|
const rawBaseUrl =
|
||||||
export const API_BASE_URL =
|
(typeof process !== 'undefined' && process.env?.VITE_API_BASE_URL) || '/api'
|
||||||
import.meta.env.VITE_API_BASE_URL || '/api'
|
|
||||||
|
export const API_BASE_URL = String(rawBaseUrl).trim().replace(/\/+$/, '')
|
||||||
|
const AUTH_REFRESH_PATH = '/auth/refresh'
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: API_BASE_URL,
|
baseURL: API_BASE_URL,
|
||||||
@@ -15,20 +16,69 @@ const api = axios.create({
|
|||||||
withCredentials: true
|
withCredentials: true
|
||||||
})
|
})
|
||||||
|
|
||||||
/* ============================
|
function isPublicPath(url) {
|
||||||
REQUEST INTERCEPTOR
|
return (
|
||||||
============================ */
|
url.startsWith('/auth/login') ||
|
||||||
|
url.startsWith(AUTH_REFRESH_PATH) ||
|
||||||
|
url.startsWith('/password/forgot') ||
|
||||||
|
url.startsWith('/password/reset')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractToken(payload) {
|
||||||
|
return (
|
||||||
|
payload?.token ||
|
||||||
|
payload?.access_token ||
|
||||||
|
payload?.data?.token ||
|
||||||
|
payload?.data?.access_token ||
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHtmlLike(value) {
|
||||||
|
const text = String(value || '').trim().toLowerCase()
|
||||||
|
return text.startsWith('<!doctype html') || text.startsWith('<html')
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeApiErrorDetail(detail, status) {
|
||||||
|
const normalized = String(detail || '').trim()
|
||||||
|
|
||||||
|
if (!normalized) {
|
||||||
|
if (status === 504) {
|
||||||
|
return 'Gateway timeout: origin server did not respond in time.'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHtmlLike(normalized)) {
|
||||||
|
if (status === 504) {
|
||||||
|
return 'Gateway timeout: origin server did not respond in time.'
|
||||||
|
}
|
||||||
|
if (status >= 500) {
|
||||||
|
return `Upstream server error (${status}).`
|
||||||
|
}
|
||||||
|
return `Unexpected HTML error response (${status || '-'})`
|
||||||
|
}
|
||||||
|
|
||||||
|
const compact = normalized.replace(/\s+/g, ' ').trim()
|
||||||
|
if (compact.length > 320) {
|
||||||
|
return `${compact.slice(0, 320)}...`
|
||||||
|
}
|
||||||
|
|
||||||
|
return compact
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectToLogin() {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
if (window.location.hash === '#/login') return
|
||||||
|
window.location.hash = '/login'
|
||||||
|
}
|
||||||
|
|
||||||
api.interceptors.request.use((config) => {
|
api.interceptors.request.use((config) => {
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const url = config.url || ''
|
const url = config.url || ''
|
||||||
|
|
||||||
const isPublic =
|
if (!isPublicPath(url) && auth?.token) {
|
||||||
url.startsWith('/auth/login') ||
|
|
||||||
url.startsWith('/auth/refresh') ||
|
|
||||||
url.startsWith('/password/forgot') ||
|
|
||||||
url.startsWith('/password/reset')
|
|
||||||
|
|
||||||
if (!isPublic && auth?.token) {
|
|
||||||
config.headers ||= {}
|
config.headers ||= {}
|
||||||
config.headers.Authorization = `Bearer ${auth.token}`
|
config.headers.Authorization = `Bearer ${auth.token}`
|
||||||
}
|
}
|
||||||
@@ -36,31 +86,113 @@ api.interceptors.request.use((config) => {
|
|||||||
return config
|
return config
|
||||||
})
|
})
|
||||||
|
|
||||||
/* ============================
|
|
||||||
RESPONSE INTERCEPTOR
|
|
||||||
============================ */
|
|
||||||
let isLoggingOut = false
|
let isLoggingOut = false
|
||||||
|
let refreshPromise = null
|
||||||
|
|
||||||
|
async function refreshAccessToken() {
|
||||||
|
if (refreshPromise) {
|
||||||
|
return refreshPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshPromise = (async () => {
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const response = await api.post(
|
||||||
|
AUTH_REFRESH_PATH,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
skipAuthRefresh: true,
|
||||||
|
skipAutoLogout: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const rawToken = extractToken(response?.data)
|
||||||
|
const token = typeof rawToken === 'string' ? rawToken.trim() : ''
|
||||||
|
const isJwt = token.split('.').length === 3
|
||||||
|
|
||||||
|
if (!token || !isJwt) {
|
||||||
|
throw new Error('Invalid refresh token response')
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.setSession({
|
||||||
|
token,
|
||||||
|
user: auth.user || null
|
||||||
|
})
|
||||||
|
|
||||||
|
return token
|
||||||
|
})().finally(() => {
|
||||||
|
refreshPromise = null
|
||||||
|
})
|
||||||
|
|
||||||
|
return refreshPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSessionAndRedirect() {
|
||||||
|
if (isLoggingOut) return
|
||||||
|
|
||||||
api.interceptors.response.use(
|
|
||||||
r => r,
|
|
||||||
async (error) => {
|
|
||||||
if (error?.response?.status === 401 && !isLoggingOut) {
|
|
||||||
isLoggingOut = true
|
isLoggingOut = true
|
||||||
try {
|
try {
|
||||||
useAuthStore().clearSession()
|
useAuthStore().clearSession()
|
||||||
} finally {
|
} finally {
|
||||||
isLoggingOut = false
|
isLoggingOut = false
|
||||||
|
redirectToLogin()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
api.interceptors.response.use(
|
||||||
|
r => r,
|
||||||
|
async (error) => {
|
||||||
|
const requestConfig = error?.config || {}
|
||||||
|
const status = error?.response?.status
|
||||||
|
const requestUrl = String(requestConfig.url || '')
|
||||||
|
const hasBlob = typeof Blob !== 'undefined' && error?.response?.data instanceof Blob
|
||||||
|
const isPasswordChangeRequest =
|
||||||
|
requestUrl.startsWith('/password/change') ||
|
||||||
|
requestUrl.startsWith('/me/password')
|
||||||
|
const isPublicRequest = isPublicPath(requestUrl)
|
||||||
|
|
||||||
|
if ((status >= 500 || hasBlob) && error) {
|
||||||
|
const method = String(requestConfig.method || 'GET').toUpperCase()
|
||||||
|
const detail = sanitizeApiErrorDetail(
|
||||||
|
await extractApiErrorDetail(error),
|
||||||
|
status
|
||||||
|
)
|
||||||
|
error.parsedMessage = detail
|
||||||
|
console.error(`API ${status || '-'} ${method} ${requestUrl}: ${detail}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldTryRefresh =
|
||||||
|
status === 401 &&
|
||||||
|
!requestConfig._retry &&
|
||||||
|
!requestConfig.skipAuthRefresh &&
|
||||||
|
!isPasswordChangeRequest &&
|
||||||
|
!isPublicRequest
|
||||||
|
|
||||||
|
if (shouldTryRefresh) {
|
||||||
|
requestConfig._retry = true
|
||||||
|
try {
|
||||||
|
const newToken = await refreshAccessToken()
|
||||||
|
requestConfig.headers ||= {}
|
||||||
|
requestConfig.headers.Authorization = `Bearer ${newToken}`
|
||||||
|
return api(requestConfig)
|
||||||
|
} catch {
|
||||||
|
// fall through to logout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password change endpoints may return 401 for business reasons
|
||||||
|
// (for example current password mismatch). Keep session in that case.
|
||||||
|
if (
|
||||||
|
status === 401 &&
|
||||||
|
!isPasswordChangeRequest &&
|
||||||
|
!requestConfig.skipAutoLogout
|
||||||
|
) {
|
||||||
|
clearSessionAndRedirect()
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
/* ============================
|
|
||||||
HELPERS
|
|
||||||
============================ */
|
|
||||||
|
|
||||||
export const get = (u, p = {}, c = {}) =>
|
export const get = (u, p = {}, c = {}) =>
|
||||||
api.get(u, { params: p, ...c }).then(r => r.data)
|
api.get(u, { params: p, ...c }).then(r => r.data)
|
||||||
|
|
||||||
@@ -73,11 +205,75 @@ export const put = (u, b = {}, c = {}) =>
|
|||||||
export const del = (u, p = {}, c = {}) =>
|
export const del = (u, p = {}, c = {}) =>
|
||||||
api.delete(u, { params: p, ...c }).then(r => r.data)
|
api.delete(u, { params: p, ...c }).then(r => r.data)
|
||||||
|
|
||||||
export const download = (u, p = {}, c = {}) =>
|
async function parseBlobErrorMessage(data) {
|
||||||
api.get(u, {
|
if (!data) return ''
|
||||||
params: p,
|
|
||||||
responseType: 'blob',
|
if (typeof Blob !== 'undefined' && data instanceof Blob) {
|
||||||
...c
|
try {
|
||||||
}).then(r => r.data)
|
const text = (await data.text())?.trim()
|
||||||
|
if (!text) return ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(text)
|
||||||
|
return (
|
||||||
|
json?.detail ||
|
||||||
|
json?.message ||
|
||||||
|
json?.error ||
|
||||||
|
text
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data === 'string') return data.trim()
|
||||||
|
if (typeof data === 'object') {
|
||||||
|
return (
|
||||||
|
data?.detail ||
|
||||||
|
data?.message ||
|
||||||
|
data?.error ||
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function extractApiErrorDetail(err) {
|
||||||
|
const status = err?.response?.status
|
||||||
|
let detail =
|
||||||
|
err?.parsedMessage ||
|
||||||
|
err?.response?.data?.detail ||
|
||||||
|
err?.response?.data?.message ||
|
||||||
|
err?.response?.data?.error ||
|
||||||
|
''
|
||||||
|
|
||||||
|
if (!detail) {
|
||||||
|
detail = await parseBlobErrorMessage(err?.response?.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!detail) {
|
||||||
|
detail = err?.message || 'Request failed'
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitizeApiErrorDetail(detail, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const download = async (u, p = {}, c = {}) => {
|
||||||
|
try {
|
||||||
|
const r = await api.get(u, { params: p, responseType: 'blob', ...c })
|
||||||
|
return r.data
|
||||||
|
} catch (err) {
|
||||||
|
const detail = await extractApiErrorDetail(err)
|
||||||
|
|
||||||
|
const wrapped = new Error(detail)
|
||||||
|
wrapped.status = err?.response?.status
|
||||||
|
wrapped.original = err
|
||||||
|
throw wrapped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default api
|
export default api
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// src/stores/userDetailStore.js
|
// src/stores/userDetailStore.js
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import api, { get, post, put } from 'src/services/api'
|
import api, { get, post, put, del } from 'src/services/api'
|
||||||
|
|
||||||
export const useUserDetailStore = defineStore('userDetail', {
|
export const useUserDetailStore = defineStore('userDetail', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@@ -11,6 +11,7 @@ export const useUserDetailStore = defineStore('userDetail', {
|
|||||||
/* ================= FLAGS ================= */
|
/* ================= FLAGS ================= */
|
||||||
loading: false,
|
loading: false,
|
||||||
saving: false,
|
saving: false,
|
||||||
|
deleting: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
|
||||||
/* ================= FORM ================= */
|
/* ================= FORM ================= */
|
||||||
@@ -23,9 +24,9 @@ export const useUserDetailStore = defineStore('userDetail', {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
address: '',
|
address: '',
|
||||||
roles: [],
|
roles: [],
|
||||||
departments: [],
|
departments: null,
|
||||||
piyasalar: [],
|
piyasalar: [],
|
||||||
nebim_users: []
|
nebim_users: null
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ================= LOOKUPS ================= */
|
/* ================= LOOKUPS ================= */
|
||||||
@@ -49,9 +50,9 @@ export const useUserDetailStore = defineStore('userDetail', {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
address: '',
|
address: '',
|
||||||
roles: [],
|
roles: [],
|
||||||
departments: [],
|
departments: null,
|
||||||
piyasalar: [],
|
piyasalar: [],
|
||||||
nebim_users: []
|
nebim_users: null
|
||||||
}
|
}
|
||||||
this.error = null
|
this.error = null
|
||||||
this.hasPassword = false
|
this.hasPassword = false
|
||||||
@@ -91,6 +92,14 @@ export const useUserDetailStore = defineStore('userDetail', {
|
|||||||
📦 PAYLOAD BUILDER (BACKEND SÖZLEŞMESİYLE UYUMLU)
|
📦 PAYLOAD BUILDER (BACKEND SÖZLEŞMESİYLE UYUMLU)
|
||||||
===================================================== */
|
===================================================== */
|
||||||
buildPayload () {
|
buildPayload () {
|
||||||
|
const departmentCodes = Array.isArray(this.form.departments)
|
||||||
|
? this.form.departments
|
||||||
|
: (this.form.departments ? [this.form.departments] : [])
|
||||||
|
|
||||||
|
const nebimUsernames = Array.isArray(this.form.nebim_users)
|
||||||
|
? this.form.nebim_users
|
||||||
|
: (this.form.nebim_users ? [this.form.nebim_users] : [])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: this.form.code,
|
code: this.form.code,
|
||||||
full_name: this.form.full_name,
|
full_name: this.form.full_name,
|
||||||
@@ -101,14 +110,11 @@ export const useUserDetailStore = defineStore('userDetail', {
|
|||||||
|
|
||||||
roles: this.form.roles,
|
roles: this.form.roles,
|
||||||
|
|
||||||
// ✅ TEK DEPARTMAN (string → backend array)
|
departments: departmentCodes.map(code => ({ code })),
|
||||||
departments: this.form.departments
|
|
||||||
? [{ code: this.form.departments }]
|
|
||||||
: [],
|
|
||||||
|
|
||||||
piyasalar: (this.form.piyasalar || []).map(code => ({ code })),
|
piyasalar: (this.form.piyasalar || []).map(code => ({ code })),
|
||||||
|
|
||||||
nebim_users: (this.form.nebim_users || []).map(username => {
|
nebim_users: nebimUsernames.map(username => {
|
||||||
const opt = (this.nebimUserOptions || []).find(x => x.value === username)
|
const opt = (this.nebimUserOptions || []).find(x => x.value === username)
|
||||||
return {
|
return {
|
||||||
username,
|
username,
|
||||||
@@ -137,9 +143,9 @@ export const useUserDetailStore = defineStore('userDetail', {
|
|||||||
this.form.address = data.address || ''
|
this.form.address = data.address || ''
|
||||||
|
|
||||||
this.form.roles = data.roles || []
|
this.form.roles = data.roles || []
|
||||||
this.form.departments = (data.departments || []).map(x => x.code)
|
this.form.departments = (data.departments || []).map(x => x.code)[0] || null
|
||||||
this.form.piyasalar = (data.piyasalar || []).map(x => x.code)
|
this.form.piyasalar = (data.piyasalar || []).map(x => x.code)
|
||||||
this.form.nebim_users = (data.nebim_users || []).map(x => x.username)
|
this.form.nebim_users = (data.nebim_users || []).map(x => x.username)[0] || null
|
||||||
|
|
||||||
this.hasPassword = !!data.has_password
|
this.hasPassword = !!data.has_password
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -217,6 +223,23 @@ export const useUserDetailStore = defineStore('userDetail', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* =====================================================
|
||||||
|
🗑️ DELETE USER
|
||||||
|
===================================================== */
|
||||||
|
async deleteUser (id) {
|
||||||
|
this.deleting = true
|
||||||
|
this.error = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
await del(`/users/${id}`)
|
||||||
|
} catch (e) {
|
||||||
|
this.error = 'Kullanici silinemedi'
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
this.deleting = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/* =====================================================
|
/* =====================================================
|
||||||
📚 LOOKUPS (NEW + EDIT ORTAK)
|
📚 LOOKUPS (NEW + EDIT ORTAK)
|
||||||
===================================================== */
|
===================================================== */
|
||||||
|
|||||||
@@ -3,6 +3,40 @@ import { defineStore } from 'pinia'
|
|||||||
import api from 'src/services/api'
|
import api from 'src/services/api'
|
||||||
import { usePermissionStore } from 'stores/permissionStore'
|
import { usePermissionStore } from 'stores/permissionStore'
|
||||||
|
|
||||||
|
function normalizeRoleCode (value) {
|
||||||
|
return String(value || '').trim().toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function roleCodeFromUser (user) {
|
||||||
|
if (!user || typeof user !== 'object') return ''
|
||||||
|
|
||||||
|
return normalizeRoleCode(
|
||||||
|
user.role_code ??
|
||||||
|
user.roleCode ??
|
||||||
|
user.RoleCode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeJwtPayload (token) {
|
||||||
|
const raw = String(token || '').trim()
|
||||||
|
if (!raw) return null
|
||||||
|
|
||||||
|
const parts = raw.split('.')
|
||||||
|
if (parts.length !== 3) return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const base64 = parts[1]
|
||||||
|
.replace(/-/g, '+')
|
||||||
|
.replace(/_/g, '/')
|
||||||
|
.padEnd(Math.ceil(parts[1].length / 4) * 4, '=')
|
||||||
|
|
||||||
|
const json = atob(base64)
|
||||||
|
return JSON.parse(json)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', {
|
export const useAuthStore = defineStore('auth', {
|
||||||
state: () => {
|
state: () => {
|
||||||
let user = null
|
let user = null
|
||||||
@@ -29,8 +63,13 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
mustChangePassword: s => !!s.forcePasswordChange,
|
mustChangePassword: s => !!s.forcePasswordChange,
|
||||||
|
|
||||||
// 🔥 TEK ADMIN KURALI
|
// 🔥 TEK ADMIN KURALI
|
||||||
isAdmin: s =>
|
isAdmin: s => {
|
||||||
String(s.user?.role_code || '').toLowerCase() === 'admin'
|
const fromUser = roleCodeFromUser(s.user)
|
||||||
|
if (fromUser) return fromUser === 'admin'
|
||||||
|
|
||||||
|
const payload = decodeJwtPayload(s.token)
|
||||||
|
return normalizeRoleCode(payload?.role_code) === 'admin'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
@@ -39,7 +78,7 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
========================================================= */
|
========================================================= */
|
||||||
setSession ({ token, user }) {
|
setSession ({ token, user }) {
|
||||||
this.token = token
|
this.token = token
|
||||||
this.user = user ?? null
|
this.user = user || null
|
||||||
this.forcePasswordChange = !!user?.force_password_change
|
this.forcePasswordChange = !!user?.force_password_change
|
||||||
|
|
||||||
localStorage.setItem('token', token)
|
localStorage.setItem('token', token)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// src/stores/downloadstHeadStore.js
|
// src/stores/downloadstHeadStore.js
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { download } from 'src/services/api'
|
import { download, extractApiErrorDetail } from 'src/services/api'
|
||||||
|
|
||||||
export const useDownloadstHeadStore = defineStore('downloadstHead', {
|
export const useDownloadstHeadStore = defineStore('downloadstHead', {
|
||||||
actions: {
|
actions: {
|
||||||
@@ -37,12 +37,14 @@ export const useDownloadstHeadStore = defineStore('downloadstHead', {
|
|||||||
return { ok: true, message: '📄 PDF hazırlandı' }
|
return { ok: true, message: '📄 PDF hazırlandı' }
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('❌ PDF açma hatası:', err)
|
const detail = await extractApiErrorDetail(err)
|
||||||
|
const status = err?.status || err?.response?.status || '-'
|
||||||
|
console.error(`? PDF a<>ma hatas<61> [${status}] /exportstamentheaderreport-pdf: ${detail}`)
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
message:
|
message:
|
||||||
err?.message ||
|
detail ||
|
||||||
'❌ PDF açma hatası'
|
'PDF a<EFBFBD>ma hatas<EFBFBD>'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// src/stores/downloadstpdfStore.js
|
// src/stores/downloadstpdfStore.js
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { download } from 'src/services/api'
|
import { download, extractApiErrorDetail } from 'src/services/api'
|
||||||
|
|
||||||
export const useDownloadstpdfStore = defineStore('downloadstpdf', {
|
export const useDownloadstpdfStore = defineStore('downloadstpdf', {
|
||||||
actions: {
|
actions: {
|
||||||
@@ -37,13 +37,15 @@ export const useDownloadstpdfStore = defineStore('downloadstpdf', {
|
|||||||
return { ok: true, message: '📄 PDF hazırlandı' }
|
return { ok: true, message: '📄 PDF hazırlandı' }
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('❌ PDF açma hatası:', err)
|
const detail = await extractApiErrorDetail(err)
|
||||||
|
const status = err?.status || err?.response?.status || '-'
|
||||||
|
console.error(`? PDF a<>ma hatas<61> [${status}] /export-pdf: ${detail}`)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
message:
|
message:
|
||||||
err?.message ||
|
detail ||
|
||||||
'❌ PDF alınamadı'
|
'PDF al<EFBFBD>namad<EFBFBD>'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
=========================================================== */
|
=========================================================== */
|
||||||
|
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import api from 'src/services/api'
|
import api, { extractApiErrorDetail } from 'src/services/api'
|
||||||
import dayjs from 'src/boot/dayjs'
|
import dayjs from 'src/boot/dayjs'
|
||||||
import { ref, toRaw, nextTick } from 'vue' // ✅ düzeltildi
|
import { ref, toRaw, nextTick } from 'vue' // ✅ düzeltildi
|
||||||
import { useAuthStore } from 'src/stores/authStore'
|
import { useAuthStore } from 'src/stores/authStore'
|
||||||
@@ -44,11 +44,11 @@ export function buildComboKey(row, beden) {
|
|||||||
|
|
||||||
export const BEDEN_SCHEMA = [
|
export const BEDEN_SCHEMA = [
|
||||||
{ 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: 'YAŞ', 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: 'GÖMLEK', 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 ELBİSE', values: ['44','46','48','50','52','54','56','58','60','62','64','66','68','70','72','74'] },
|
{ 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', '110CM', '115CM', '120CM', '125CM', '130CM', '135CM'] }
|
{ key: 'aksbir', title: 'AKSESUAR', values: [' ', '44', 'STD', '110', '115', '120', '125', '130', '135'] }
|
||||||
]
|
]
|
||||||
|
|
||||||
export const schemaByKey = BEDEN_SCHEMA.reduce((m, g) => {
|
export const schemaByKey = BEDEN_SCHEMA.reduce((m, g) => {
|
||||||
@@ -268,7 +268,7 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
if (!Array.isArray(invalidList) || invalidList.length === 0) return
|
if (!Array.isArray(invalidList) || invalidList.length === 0) return
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
$q.dialog({
|
const dlg = $q.dialog({
|
||||||
title: '🚨 Tanımsız Ürün Kombinasyonları',
|
title: '🚨 Tanımsız Ürün Kombinasyonları',
|
||||||
message: `
|
message: `
|
||||||
<div style="max-height:60vh;overflow:auto">
|
<div style="max-height:60vh;overflow:auto">
|
||||||
@@ -309,8 +309,10 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
})
|
})
|
||||||
.onOk(() => resolve())
|
.onOk(() => resolve())
|
||||||
.onDismiss(() => resolve())
|
.onDismiss(() => resolve())
|
||||||
.onShown(() => {
|
|
||||||
// Satıra tıklama → scroll + highlight
|
// Quasar v2 chain API'de onShown yok; dialog DOM'u render olduktan sonra bağla.
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!dlg) return
|
||||||
const nodes = document.querySelectorAll('.invalid-row')
|
const nodes = document.querySelectorAll('.invalid-row')
|
||||||
nodes.forEach(n => {
|
nodes.forEach(n => {
|
||||||
n.addEventListener('click', () => {
|
n.addEventListener('click', () => {
|
||||||
@@ -318,7 +320,7 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
this.scrollToInvalidRow?.(ck)
|
this.scrollToInvalidRow?.(ck)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
}, 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
,
|
,
|
||||||
@@ -389,8 +391,10 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
})
|
})
|
||||||
return resp.data
|
return resp.data
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ fetchOrderPdf hata:", err)
|
const detail = await extractApiErrorDetail(err)
|
||||||
throw err
|
const status = err?.status || err?.response?.status || '-'
|
||||||
|
console.error(`? fetchOrderPdf hata [${status}] order=${orderId}: ${detail}`)
|
||||||
|
throw new Error(detail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
,
|
,
|
||||||
@@ -414,8 +418,11 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
setTimeout(() => URL.revokeObjectURL(url), 60_000)
|
setTimeout(() => URL.revokeObjectURL(url), 60_000)
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('❌ PDF açma hatası:', err)
|
const detail = await extractApiErrorDetail(err)
|
||||||
throw err
|
const orderId = id || this.header?.OrderHeaderID || '-'
|
||||||
|
const status = err?.status || err?.response?.status || '-'
|
||||||
|
console.error(`? PDF a<>ma hatas<61> [${status}] order=${orderId}: ${detail}`)
|
||||||
|
throw new Error(detail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1817,7 +1824,9 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
// MERGE: bedenleri topluyoruz (override değil)
|
// MERGE: bedenleri topluyoruz (override değil)
|
||||||
const merged = { ...(prevMap || {}) }
|
const merged = { ...(prevMap || {}) }
|
||||||
for (const [k, v] of Object.entries(newMap || {})) {
|
for (const [k, v] of Object.entries(newMap || {})) {
|
||||||
const beden = (k == null || String(k).trim() === '') ? ' ' : String(k).trim()
|
const beden = (k == null || String(k).trim() === '')
|
||||||
|
? ' '
|
||||||
|
: normalizeBedenLabel(String(k))
|
||||||
merged[beden] = Number(merged[beden] || 0) + Number(v || 0)
|
merged[beden] = Number(merged[beden] || 0) + Number(v || 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2231,11 +2240,11 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
|
|
||||||
merged[modelKey] ??= []
|
merged[modelKey] ??= []
|
||||||
|
|
||||||
const beden = (
|
const bedenRaw =
|
||||||
raw.ItemDim1Code == null || String(raw.ItemDim1Code).trim() === ''
|
raw.ItemDim1Code == null
|
||||||
? ''
|
? ''
|
||||||
: String(raw.ItemDim1Code).trim().toUpperCase()
|
: String(raw.ItemDim1Code).trim()
|
||||||
)
|
const beden = bedenRaw === '' ? ' ' : normalizeBedenLabel(bedenRaw)
|
||||||
|
|
||||||
const qty = Number(raw.Qty1 || raw.Qty || 0)
|
const qty = Number(raw.Qty1 || raw.Qty || 0)
|
||||||
|
|
||||||
@@ -2310,8 +2319,25 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
row.kategori
|
row.kategori
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const cleanedMap = { ...row.__tmpMap }
|
||||||
|
const hasNonBlankBeden = Object.keys(cleanedMap)
|
||||||
|
.some(k => String(k).trim() !== '')
|
||||||
|
|
||||||
|
// Gomlek/takim/pantolon gibi gruplarda bos beden sadece legacy kirli veri olabilir.
|
||||||
|
// Gecerli bedenler varsa bos bedeni payload/UI'dan temizliyoruz.
|
||||||
|
if (grpKey !== 'aksbir' && hasNonBlankBeden) {
|
||||||
|
delete cleanedMap[' ']
|
||||||
|
delete cleanedMap['']
|
||||||
|
if (row.lineIdMap && typeof row.lineIdMap === 'object') {
|
||||||
|
delete row.lineIdMap[' ']
|
||||||
|
delete row.lineIdMap['']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
row.grpKey = grpKey
|
row.grpKey = grpKey
|
||||||
row.bedenMap = { [grpKey]: { ...row.__tmpMap } }
|
row.bedenMap = { [grpKey]: { ...cleanedMap } }
|
||||||
|
row.adet = Object.values(cleanedMap).reduce((a, b) => a + (Number(b) || 0), 0)
|
||||||
|
row.tutar = Number((row.adet * Number(row.fiyat || 0)).toFixed(2))
|
||||||
|
|
||||||
/* ===================================================
|
/* ===================================================
|
||||||
🔒 AKSBİR — BOŞLUK BEDEN GERÇEK ADETİ ALIR
|
🔒 AKSBİR — BOŞLUK BEDEN GERÇEK ADETİ ALIR
|
||||||
@@ -2485,9 +2511,9 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
const g = (row?.urunAnaGrubu || '').toUpperCase()
|
const g = (row?.urunAnaGrubu || '').toUpperCase()
|
||||||
if (g.includes('TAKIM')) return 'tak'
|
if (g.includes('TAKIM')) return 'tak'
|
||||||
if (g.includes('PANTOLON')) return 'pan'
|
if (g.includes('PANTOLON')) return 'pan'
|
||||||
if (g.includes('GÖMLEK')) return 'gom'
|
if (g.includes('GOMLEK')) return 'gom'
|
||||||
if (g.includes('AYAKKABI')) return 'ayk'
|
if (g.includes('AYAKKABI')) return 'ayk'
|
||||||
if (g.includes('YAŞ')) return 'yas'
|
if (g.includes('YAS')) return 'yas'
|
||||||
return 'tak'
|
return 'tak'
|
||||||
},
|
},
|
||||||
/* =======================================================
|
/* =======================================================
|
||||||
@@ -2725,7 +2751,19 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
======================================================= */
|
======================================================= */
|
||||||
if (choice === 'print') {
|
if (choice === 'print') {
|
||||||
const id = this.header?.OrderHeaderID || serverOrderId
|
const id = this.header?.OrderHeaderID || serverOrderId
|
||||||
if (id) await this.downloadOrderPdf(id)
|
if (id) {
|
||||||
|
try {
|
||||||
|
await this.downloadOrderPdf(id)
|
||||||
|
} catch (pdfErr) {
|
||||||
|
console.error('⚠️ PDF açılamadı, kayıt başarılı:', pdfErr)
|
||||||
|
$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message:
|
||||||
|
pdfErr?.message ||
|
||||||
|
'Sipariş kaydedildi fakat PDF açılamadı.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2743,6 +2781,7 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
message:
|
message:
|
||||||
|
err?.response?.data?.detail ||
|
||||||
err?.response?.data?.message ||
|
err?.response?.data?.message ||
|
||||||
err?.message ||
|
err?.message ||
|
||||||
'Kayıt sırasında hata'
|
'Kayıt sırasında hata'
|
||||||
@@ -3054,6 +3093,12 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
|
|
||||||
/* 🔹 BEDENSİZ / AKSBİR */
|
/* 🔹 BEDENSİZ / AKSBİR */
|
||||||
if (!hasAnyBeden) {
|
if (!hasAnyBeden) {
|
||||||
|
const allowBlankPayload =
|
||||||
|
grpKey === 'aksbir' || row._deleteSignal === true
|
||||||
|
if (!allowBlankPayload) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const qty = toNum(row.qty ?? row.Qty1 ?? row.miktar ?? 0)
|
const qty = toNum(row.qty ?? row.Qty1 ?? row.miktar ?? 0)
|
||||||
|
|
||||||
// ✅ ComboKey stabil: bedenKey = '_'
|
// ✅ ComboKey stabil: bedenKey = '_'
|
||||||
@@ -3067,6 +3112,7 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
orderLineId =
|
orderLineId =
|
||||||
safeStr(lineIdMap?.[bedenKey]) ||
|
safeStr(lineIdMap?.[bedenKey]) ||
|
||||||
safeStr(lineIdMap?.[bedenPayload]) ||
|
safeStr(lineIdMap?.[bedenPayload]) ||
|
||||||
|
safeStr(lineIdMap?.[' ']) ||
|
||||||
safeStr(row.OrderLineID)
|
safeStr(row.OrderLineID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3083,6 +3129,15 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
|
|
||||||
/* 🔹 BEDENLİ */
|
/* 🔹 BEDENLİ */
|
||||||
for (const [bedenRaw, qtyRaw] of Object.entries(map)) {
|
for (const [bedenRaw, qtyRaw] of Object.entries(map)) {
|
||||||
|
const isBlankBeden = safeStr(bedenRaw) === ''
|
||||||
|
if (
|
||||||
|
isBlankBeden &&
|
||||||
|
grpKey !== 'aksbir' &&
|
||||||
|
row._deleteSignal !== true
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const qty = toNum(qtyRaw)
|
const qty = toNum(qtyRaw)
|
||||||
|
|
||||||
// ✅ payload beden: '' / 'S' / 'M' ...
|
// ✅ payload beden: '' / 'S' / 'M' ...
|
||||||
@@ -3096,6 +3151,7 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
orderLineId =
|
orderLineId =
|
||||||
safeStr(lineIdMap?.[bedenKey]) ||
|
safeStr(lineIdMap?.[bedenKey]) ||
|
||||||
safeStr(lineIdMap?.[bedenPayload]) ||
|
safeStr(lineIdMap?.[bedenPayload]) ||
|
||||||
|
safeStr(lineIdMap?.[' ']) ||
|
||||||
(Object.keys(map).length === 1
|
(Object.keys(map).length === 1
|
||||||
? safeStr(row.OrderLineID)
|
? safeStr(row.OrderLineID)
|
||||||
: '')
|
: '')
|
||||||
@@ -3220,85 +3276,133 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
|
|
||||||
|
|
||||||
/* ===========================================================
|
/* ===========================================================
|
||||||
🔹 BEDEN LABEL NORMALİZASYONU (exported helper)
|
Size Label Normalization (frontend helper)
|
||||||
=========================================================== */
|
=========================================================== */
|
||||||
|
function safeTrimUpperJs(v) {
|
||||||
|
return (v == null ? '' : String(v)).trim().toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTextForMatch(v) {
|
||||||
|
return safeTrimUpperJs(v)
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNumericSizeJs(v) {
|
||||||
|
const s = safeTrimUpperJs(v)
|
||||||
|
if (s === '' || !/^\d+$/.test(s)) return null
|
||||||
|
const n = Number.parseInt(s, 10)
|
||||||
|
return Number.isNaN(n) ? null : n
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeBedenLabel(v) {
|
export function normalizeBedenLabel(v) {
|
||||||
if (v === null || v === undefined) return ' '
|
let s = (v == null ? '' : String(v)).trim()
|
||||||
let s = String(v).trim()
|
|
||||||
if (s === '') return ' '
|
if (s === '') return ' '
|
||||||
// 44R, 50L vb. son ekleri at
|
|
||||||
s = s.replace(/(^\d+)\s*[A-Z]?$/i, '$1')
|
|
||||||
s = s.toUpperCase()
|
s = s.toUpperCase()
|
||||||
|
|
||||||
// harfli bedenlerin normalizasyonu
|
// Backend parity: normalize common "standard size" aliases.
|
||||||
const map = {
|
switch (s) {
|
||||||
'XS': 'XS', 'S': 'S', 'M': 'M', 'L': 'L', 'XL': 'XL',
|
case 'STD':
|
||||||
'XXL': '2XL', '2XL': '2XL', '3XL': '3XL', '4XL': '4XL',
|
case 'STANDART':
|
||||||
'5XL': '5XL', '6XL': '6XL', '7XL': '7XL', 'STD': 'STD'
|
case 'STANDARD':
|
||||||
|
case 'ONE SIZE':
|
||||||
|
case 'ONESIZE':
|
||||||
|
return 'STD'
|
||||||
}
|
}
|
||||||
if (map[s]) return map[s]
|
|
||||||
|
|
||||||
// tamamen sayıysa string olarak döndür
|
// Backend parity: only values ending with CM are converted to numeric part.
|
||||||
if (/^\d+$/.test(s)) return s
|
if (s.endsWith('CM')) {
|
||||||
|
const num = s.slice(0, -2).trim()
|
||||||
|
if (num !== '') return num
|
||||||
|
}
|
||||||
|
|
||||||
// virgüllü değer geldiyse ilkini al
|
switch (s) {
|
||||||
if (s.includes(',')) return s.split(',')[0].trim()
|
case 'XS':
|
||||||
|
case 'S':
|
||||||
|
case 'M':
|
||||||
|
case 'L':
|
||||||
|
case 'XL':
|
||||||
|
case '2XL':
|
||||||
|
case '3XL':
|
||||||
|
case '4XL':
|
||||||
|
case '5XL':
|
||||||
|
case '6XL':
|
||||||
|
case '7XL':
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===========================================================
|
return s
|
||||||
🔹 BEDEN GRUBU ALGILAMA HELPER’I
|
|
||||||
-----------------------------------------------------------
|
|
||||||
Gelen beden listesini, ürün grubu/kategori bilgisine göre
|
|
||||||
doğru grup anahtarına dönüştürür (ayk, yas, pan, gom, tak, aksbir).
|
|
||||||
-----------------------------------------------------------
|
|
||||||
=========================================================== */
|
|
||||||
export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '') {
|
|
||||||
const list = Array.isArray(bedenList) && bedenList.length > 0
|
|
||||||
? bedenList.map(v => (v || '').toString().trim().toUpperCase())
|
|
||||||
: [' ']
|
|
||||||
|
|
||||||
const ana = (urunAnaGrubu || '')
|
|
||||||
.toUpperCase()
|
|
||||||
.trim()
|
|
||||||
.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 giyimGruplari = ['GÖMLEK','CEKET','PANTOLON','MONT','YELEK','TAKIM','TSHIRT','TİŞÖRT']
|
|
||||||
// 🔸 Pantolon özel durumu
|
|
||||||
if (
|
|
||||||
aksesuarGruplari.some(g => ana.includes(g) || kat.includes(g)) &&
|
|
||||||
!giyimGruplari.some(g => ana.includes(g))
|
|
||||||
) 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'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔸 Yaş grubu (çocuk/garson)
|
// Backward-compatible alias kept for older call sites/bundles.
|
||||||
if (kat.includes('GARSON') || kat.includes('ÇOCUK')) return 'yas'
|
export function normalizeBeden(v) {
|
||||||
|
return normalizeBedenLabel(v)
|
||||||
|
}
|
||||||
|
|
||||||
// 🔸 Harfli beden varsa doğrudan "gom" (gömlek, üst giyim)
|
/* ===========================================================
|
||||||
const harfliBedenler = ['XS','S','M','L','XL','XXL','3XL','4XL']
|
Size Group Detection
|
||||||
if (list.some(b => harfliBedenler.includes(b))) return 'gom'
|
- Core logic aligned with backend detectBedenGroupGo
|
||||||
|
- Keeps frontend aksbir bucket for accessory lines
|
||||||
|
=========================================================== */
|
||||||
|
export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '') {
|
||||||
|
const list = Array.isArray(bedenList) ? bedenList : []
|
||||||
|
const ana = normalizeTextForMatch(urunAnaGrubu)
|
||||||
|
const alt = normalizeTextForMatch(urunKategori)
|
||||||
|
|
||||||
|
// Frontend compatibility: accessory-only products should stay in aksbir.
|
||||||
|
const accessoryGroups = [
|
||||||
|
'AKSESUAR', 'KRAVAT', 'PAPYON', 'KEMER', 'CORAP',
|
||||||
|
'FULAR', 'MENDIL', 'KASKOL', 'ASKI', 'YAKA', 'KOL DUGMESI'
|
||||||
|
]
|
||||||
|
const clothingGroups = ['GOMLEK', 'CEKET', 'PANTOLON', 'MONT', 'YELEK', 'TAKIM', 'TSHIRT']
|
||||||
|
if (
|
||||||
|
accessoryGroups.some(g => ana.includes(g) || alt.includes(g)) &&
|
||||||
|
!clothingGroups.some(g => ana.includes(g))
|
||||||
|
) {
|
||||||
|
return 'aksbir'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ana.includes('AYAKKABI') || alt.includes('AYAKKABI')) {
|
||||||
|
return 'ayk'
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasYasNumeric = false
|
||||||
|
let hasAykNumeric = false
|
||||||
|
let hasPanNumeric = false
|
||||||
|
|
||||||
|
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'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3316,7 +3420,7 @@ export function toSummaryRowFromForm(form) {
|
|||||||
const lbl =
|
const lbl =
|
||||||
rawLbl == null || String(rawLbl).trim() === ''
|
rawLbl == null || String(rawLbl).trim() === ''
|
||||||
? ' '
|
? ' '
|
||||||
: String(rawLbl).trim()
|
: normalizeBedenLabel(String(rawLbl))
|
||||||
|
|
||||||
const val = Number(values[i] || 0)
|
const val = Number(values[i] || 0)
|
||||||
if (val > 0) {
|
if (val > 0) {
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user