Compare commits

...

112 Commits

Author SHA1 Message Date
M_Kececi
1ced1b1649 Merge remote-tracking branch 'origin/master' 2026-02-19 12:28:13 +03:00
M_Kececi
76e7ca2e4a Merge remote-tracking branch 'origin/master' 2026-02-19 09:32:26 +03:00
M_Kececi
ed81fdf84f Merge remote-tracking branch 'origin/master' 2026-02-19 09:03:21 +03:00
M_Kececi
026c40c0b3 Merge remote-tracking branch 'origin/master' 2026-02-19 02:00:49 +03:00
M_Kececi
0136e6638b Merge remote-tracking branch 'origin/master' 2026-02-19 01:34:56 +03:00
M_Kececi
7184a40dd3 Merge remote-tracking branch 'origin/master' 2026-02-18 17:35:15 +03:00
M_Kececi
de58ef1043 Merge remote-tracking branch 'origin/master' 2026-02-18 16:58:46 +03:00
M_Kececi
744e20591d Merge remote-tracking branch 'origin/master' 2026-02-18 16:40:55 +03:00
M_Kececi
1263531edd Merge remote-tracking branch 'origin/master' 2026-02-18 16:40:37 +03:00
M_Kececi
d2bd0684c1 Merge remote-tracking branch 'origin/master' 2026-02-18 15:44:51 +03:00
M_Kececi
13f8801379 Merge remote-tracking branch 'origin/master' 2026-02-18 15:17:46 +03:00
M_Kececi
c3a1627152 Merge remote-tracking branch 'origin/master' 2026-02-18 15:09:47 +03:00
M_Kececi
727069910d Merge remote-tracking branch 'origin/master' 2026-02-18 14:34:21 +03:00
M_Kececi
1f95099677 Merge remote-tracking branch 'origin/master' 2026-02-18 14:24:50 +03:00
M_Kececi
dc36699a2b Merge remote-tracking branch 'origin/master' 2026-02-18 14:11:37 +03:00
M_Kececi
0e63370810 Merge remote-tracking branch 'origin/master' 2026-02-18 14:07:26 +03:00
M_Kececi
ea7d426436 Merge remote-tracking branch 'origin/master' 2026-02-18 14:02:58 +03:00
M_Kececi
369db87091 Merge remote-tracking branch 'origin/master' 2026-02-18 14:02:54 +03:00
M_Kececi
400220995b Merge remote-tracking branch 'origin/master' 2026-02-18 13:51:34 +03:00
M_Kececi
eff80a3211 Merge remote-tracking branch 'origin/master' 2026-02-17 15:02:51 +03:00
M_Kececi
00fc69601e Merge remote-tracking branch 'origin/master' 2026-02-17 14:14:48 +03:00
M_Kececi
3d508868c8 Merge remote-tracking branch 'origin/master' 2026-02-17 14:11:28 +03:00
M_Kececi
46c617b8f5 Merge remote-tracking branch 'origin/master' 2026-02-17 13:30:04 +03:00
M_Kececi
3bbb8539c7 Merge remote-tracking branch 'origin/master' 2026-02-17 13:21:00 +03:00
M_Kececi
5ca00065e6 fix(deploy): stabilize sass-embedded and runtime env files 2026-02-17 13:18:21 +03:00
M_Kececi
93446e6a69 Merge remote-tracking branch 'origin/master' 2026-02-17 13:13:25 +03:00
M_Kececi
4b455814b4 Merge remote-tracking branch 'origin/master' 2026-02-17 13:07:18 +03:00
M_Kececi
291603163b Merge remote-tracking branch 'origin/master' 2026-02-17 13:01:00 +03:00
M_Kececi
e4cdae58d4 fix(deploy): remove embedded install from script; pin sass-embedded linux runtime 2026-02-17 12:56:14 +03:00
M_Kececi
6483678267 Merge remote-tracking branch 'origin/master' 2026-02-17 12:49:56 +03:00
M_Kececi
fde9b4469f Merge remote-tracking branch 'origin/master' 2026-02-17 12:41:37 +03:00
M_Kececi
90ed98d59f Merge remote-tracking branch 'origin/master' 2026-02-17 12:35:46 +03:00
M_Kececi
88c20d844f Merge remote-tracking branch 'origin/master' 2026-02-17 12:32:04 +03:00
M_Kececi
cf8352dbaf Merge remote-tracking branch 'origin/master' 2026-02-17 12:27:55 +03:00
M_Kececi
5429305a6e Merge remote-tracking branch 'origin/master' 2026-02-17 12:26:41 +03:00
M_Kececi
88c189a48d Merge remote-tracking branch 'origin/master' 2026-02-17 12:23:24 +03:00
M_Kececi
8eaee91537 Merge remote-tracking branch 'origin/master' 2026-02-17 11:53:26 +03:00
M_Kececi
199390bc66 Merge remote-tracking branch 'origin/master' 2026-02-17 11:53:12 +03:00
M_Kececi
1c1df2521e fix: detach deploy to prevent EPIPE 2026-02-17 11:48:15 +03:00
M_Kececi
68790c9f4e fix: stable webhook deploy (EPIPE safe) 2026-02-17 11:44:32 +03:00
M_Kececi
3eac743225 Merge remote-tracking branch 'origin/master' 2026-02-17 11:41:41 +03:00
M_Kececi
d82cea0b54 Add deploy and webhook config 2026-02-17 11:29:24 +03:00
M_Kececi
8c0f18eee3 ui build 2026-02-17 11:24:55 +03:00
M_Kececi
3faaf57768 Merge remote-tracking branch 'origin/master' 2026-02-17 11:11:52 +03:00
M_Kececi
10adf327f4 Merge remote-tracking branch 'origin/master' 2026-02-17 11:11:34 +03:00
M_Kececi
47b3e9172f Merge remote-tracking branch 'origin/master' 2026-02-17 11:11:20 +03:00
Your Name
9cf575d71d Merge remote-tracking branch 'origin/master' 2026-02-17 11:06:55 +03:00
MEHMETKECECI
585e98afb8 Merge remote-tracking branch 'origin/master' 2026-02-17 10:15:17 +03:00
MEHMETKECECI
76ba649366 Ignore deploy public folder 2026-02-17 09:46:25 +03:00
MEHMETKECECI
801ed23cd9 Merge remote-tracking branch 'origin/master' 2026-02-17 09:07:49 +03:00
MEHMETKECECI
2c6515b580 add gitignore for ui build 2026-02-17 08:51:59 +03:00
MEHMETKECECI
6f842043b2 Merge remote-tracking branch 'origin/master' 2026-02-16 17:29:40 +03:00
MEHMETKECECI
7edf87055e Merge remote-tracking branch 'origin/master' 2026-02-16 17:22:14 +03:00
MEHMETKECECI
484512ff25 Merge remote-tracking branch 'origin/master' 2026-02-16 17:21:20 +03:00
MEHMETKECECI
0a14f87a3e Merge remote-tracking branch 'origin/master' 2026-02-16 16:51:45 +03:00
MEHMETKECECI
daedff2880 Merge remote-tracking branch 'origin/master' 2026-02-16 16:45:04 +03:00
MEHMETKECECI
54182e97c5 Merge remote-tracking branch 'origin/master' 2026-02-16 16:07:49 +03:00
MEHMETKECECI
14d71ba925 Merge remote-tracking branch 'origin/master' 2026-02-16 16:04:13 +03:00
MEHMETKECECI
5124ad78af Merge remote-tracking branch 'origin/master' 2026-02-16 15:53:00 +03:00
MEHMETKECECI
f8b07a6aea Merge remote-tracking branch 'origin/master' 2026-02-16 15:14:50 +03:00
MEHMETKECECI
f5f37089ac Merge remote-tracking branch 'origin/master' 2026-02-16 15:14:42 +03:00
MEHMETKECECI
4b01a0835d Merge remote-tracking branch 'origin/master' 2026-02-16 15:10:37 +03:00
MEHMETKECECI
82e51bbfcd Merge remote-tracking branch 'origin/master' 2026-02-16 15:08:37 +03:00
MEHMETKECECI
0a5ffe1407 Merge remote-tracking branch 'origin/master' 2026-02-16 15:07:21 +03:00
MEHMETKECECI
5a6350250a Merge remote-tracking branch 'origin/master' 2026-02-16 14:55:41 +03:00
MEHMETKECECI
9ce85ff6b8 Merge remote-tracking branch 'origin/master' 2026-02-16 14:18:49 +03:00
MEHMETKECECI
f6e1e7d00e fix: sanitize pdf font path 2026-02-16 14:07:38 +03:00
MEHMETKECECI
a514f4dcfa fix: enforce absolute pdf font path 2026-02-16 12:25:54 +03:00
MEHMETKECECI
2f9c917a08 fix: normalize pdf font path 2026-02-16 12:14:39 +03:00
MEHMETKECECI
cb415a6f63 Merge remote-tracking branch 'origin/master' 2026-02-16 12:08:55 +03:00
MEHMETKECECI
a1ab7508c6 Merge remote-tracking branch 'origin/master' 2026-02-16 12:03:59 +03:00
MEHMETKECECI
ec6d547641 Merge remote-tracking branch 'origin/master' 2026-02-16 11:59:14 +03:00
MEHMETKECECI
96b973b71f chore: remove runtime files from repo 2026-02-16 11:40:25 +03:00
MEHMETKECECI
5c5916f58c rebuild with new font loader 2026-02-16 11:37:59 +03:00
MEHMETKECECI
f767726617 fix: linux amd64 static build 2026-02-16 11:29:29 +03:00
MEHMETKECECI
fcd31c4d7f restore gitignore 2026-02-16 11:25:16 +03:00
MEHMETKECECI
756bbe137d build linux binary 2026-02-16 11:18:39 +03:00
MEHMETKECECI
cd8c8a6e9e Merge remote-tracking branch 'origin/master' 2026-02-16 11:10:04 +03:00
MEHMETKECECI
70f097806b Merge remote-tracking branch 'origin/master' 2026-02-16 09:46:08 +03:00
MEHMETKECECI
21db754045 Merge remote-tracking branch 'origin/master' 2026-02-16 09:35:11 +03:00
MEHMETKECECI
56e40de192 Merge remote-tracking branch 'origin/master' 2026-02-15 23:23:06 +03:00
MEHMETKECECI
f338ef1986 Merge remote-tracking branch 'origin/master' 2026-02-15 21:14:10 +03:00
MEHMETKECECI
e7a776aede Merge remote-tracking branch 'origin/master' 2026-02-15 21:13:58 +03:00
MEHMETKECECI
897b153cfc Merge remote-tracking branch 'origin/master' 2026-02-15 21:01:57 +03:00
MEHMETKECECI
babf77ae17 Merge remote-tracking branch 'origin/master' 2026-02-15 20:54:47 +03:00
MEHMETKECECI
43d018f492 Merge remote-tracking branch 'origin/master' 2026-02-15 05:37:41 +03:00
MEHMETKECECI
434908495e Merge remote-tracking branch 'origin/master' 2026-02-15 05:36:19 +03:00
MEHMETKECECI
a236686f7d Merge remote-tracking branch 'origin/master' 2026-02-15 05:31:32 +03:00
MEHMETKECECI
400fa38a49 Merge remote-tracking branch 'origin/master' 2026-02-14 21:07:17 +03:00
MEHMETKECECI
4be05573eb Merge remote-tracking branch 'origin/master' 2026-02-14 20:21:33 +03:00
MEHMETKECECI
9e18ac1398 Merge remote-tracking branch 'origin/master' 2026-02-14 20:08:56 +03:00
MEHMETKECECI
7d94573bdd Merge remote-tracking branch 'origin/master' 2026-02-14 19:59:03 +03:00
MEHMETKECECI
a2a756870d Merge remote-tracking branch 'origin/master' 2026-02-14 19:57:41 +03:00
MEHMETKECECI
d0f20674ea Merge remote-tracking branch 'origin/master' 2026-02-14 19:48:53 +03:00
MEHMETKECECI
237f73a923 Merge remote-tracking branch 'origin/master' 2026-02-14 19:44:53 +03:00
MEHMETKECECI
ce110ed86f Merge remote-tracking branch 'origin/master' 2026-02-14 16:55:10 +03:00
MEHMETKECECI
6d22f5874a Merge remote-tracking branch 'origin/master' 2026-02-14 16:44:36 +03:00
MEHMETKECECI
6105be3eb3 Merge remote-tracking branch 'origin/master' 2026-02-14 16:44:28 +03:00
MEHMETKECECI
fd5b8a2954 Merge remote-tracking branch 'origin/master' 2026-02-14 16:16:47 +03:00
MEHMETKECECI
14b3d86782 Merge remote-tracking branch 'origin/master' 2026-02-14 15:57:28 +03:00
MEHMETKECECI
f079ee80da Merge remote-tracking branch 'origin/master' 2026-02-14 15:49:40 +03:00
MEHMETKECECI
3bc3543010 Merge remote-tracking branch 'origin/master' 2026-02-14 15:28:15 +03:00
MEHMETKECECI
4355a09a15 Merge remote-tracking branch 'origin/master' 2026-02-14 15:02:54 +03:00
MEHMETKECECI
5eca170e97 Merge remote-tracking branch 'origin/master' 2026-02-14 14:59:13 +03:00
MEHMETKECECI
676724b5d5 Merge remote-tracking branch 'origin/master' 2026-02-14 14:58:13 +03:00
MEHMETKECECI
1874e4c0d3 Merge remote-tracking branch 'origin/master' 2026-02-14 14:56:17 +03:00
MEHMETKECECI
a3456d388d Merge remote-tracking branch 'origin/master' 2026-02-14 14:56:06 +03:00
MEHMETKECECI
e38421db45 Merge remote-tracking branch 'origin/master' 2026-02-14 14:53:37 +03:00
MEHMETKECECI
c6007c05cb Merge remote-tracking branch 'origin/master' 2026-02-14 14:43:41 +03:00
MEHMETKECECI
406d9e8ce5 Merge remote-tracking branch 'origin/master' 2026-02-14 11:44:15 +03:00
MEHMETKECECI
1d15b619f9 Merge remote-tracking branch 'origin/master' 2026-02-14 11:11:13 +03:00
MEHMETKECECI
7b0850eb4e Merge remote-tracking branch 'origin/master' 2026-02-14 10:40:23 +03:00
61 changed files with 3038 additions and 1167 deletions

12
.gitignore vendored Normal file
View 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
View 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
View 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"
}
}
}
]
}
}
]

View File

@@ -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
View 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
View File

@@ -0,0 +1,13 @@
# Binary
bssapp
# Env
.env
mail.env
# Runtime fonts
fonts/*.ttf
# Go
.gocache
*.exe

View File

@@ -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
} }

View File

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

Binary file not shown.

Binary file not shown.

View File

View File

@@ -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

View File

@@ -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"
} }

View File

@@ -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))
}) })

View File

@@ -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

View File

@@ -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)

View File

@@ -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
} }
} }

View File

@@ -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),

View File

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

View File

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

View File

@@ -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

View File

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

View File

@@ -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
` `

View File

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

View File

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

View File

@@ -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(&currentHash)
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,
)
} }
} }

View File

@@ -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 {
} }
// ================================================== // ==================================================
// 3MIGRATION (dfusr → mk_dfusr) // 3LEGACY 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{

View File

@@ -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(

View File

@@ -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
View 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
}

View File

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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
// ====================================================== // ======================================================

View File

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

View File

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

View File

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

10
ui/package-lock.json generated
View File

@@ -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": {

View File

@@ -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",

View File

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

View File

@@ -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'
} }
] ]
} }

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 -->

View File

@@ -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>

View File

@@ -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 () {

View File

@@ -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 -->

View File

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

View File

@@ -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({

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -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)
===================================================== */ ===================================================== */

View File

@@ -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)

View File

@@ -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>'
} }
} }
} }

View File

@@ -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>'
} }
} }
} }

View File

@@ -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 HELPERI
-----------------------------------------------------------
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) {

View File

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