Compare commits
53 Commits
3faaf57768
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66df9b0f10 | ||
|
|
b4acdf3f60 | ||
|
|
a93630df7a | ||
|
|
47ca23f970 | ||
|
|
4ca8abb52f | ||
|
|
50f87e3290 | ||
|
|
d4583c17d2 | ||
|
|
32d0c38ab9 | ||
|
|
d9c527d13f | ||
|
|
f6b9793c41 | ||
|
|
1ced1b1649 | ||
|
|
76e7ca2e4a | ||
|
|
ed81fdf84f | ||
|
|
026c40c0b3 | ||
|
|
0136e6638b | ||
|
|
7184a40dd3 | ||
|
|
de58ef1043 | ||
|
|
744e20591d | ||
|
|
1263531edd | ||
|
|
d2bd0684c1 | ||
|
|
13f8801379 | ||
|
|
c3a1627152 | ||
|
|
727069910d | ||
|
|
1f95099677 | ||
|
|
dc36699a2b | ||
|
|
0e63370810 | ||
|
|
ea7d426436 | ||
|
|
369db87091 | ||
|
|
400220995b | ||
|
|
eff80a3211 | ||
|
|
00fc69601e | ||
|
|
3d508868c8 | ||
|
|
46c617b8f5 | ||
|
|
3bbb8539c7 | ||
|
|
5ca00065e6 | ||
|
|
93446e6a69 | ||
|
|
4b455814b4 | ||
|
|
291603163b | ||
|
|
e4cdae58d4 | ||
|
|
6483678267 | ||
|
|
fde9b4469f | ||
|
|
90ed98d59f | ||
|
|
88c20d844f | ||
|
|
cf8352dbaf | ||
|
|
5429305a6e | ||
|
|
88c189a48d | ||
|
|
8eaee91537 | ||
|
|
199390bc66 | ||
|
|
1c1df2521e | ||
|
|
68790c9f4e | ||
|
|
3eac743225 | ||
|
|
d82cea0b54 | ||
|
|
8c0f18eee3 |
212
deploy/deploy.sh
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
umask 022
|
||||||
|
|
||||||
|
export NODE_OPTIONS="--max_old_space_size=4096"
|
||||||
|
export CI="true"
|
||||||
|
export npm_config_progress="false"
|
||||||
|
export npm_config_loglevel="warn"
|
||||||
|
export FORCE_COLOR="0"
|
||||||
|
|
||||||
|
LOG_FILE="/var/log/bssapp_deploy.log"
|
||||||
|
APP_DIR="/opt/bssapp"
|
||||||
|
LOCK_FILE="/tmp/bssapp_deploy.lock"
|
||||||
|
RUNTIME_BACKUP_DIR=""
|
||||||
|
RUNTIME_PRESERVE_FILES=(
|
||||||
|
".env"
|
||||||
|
"mail.env"
|
||||||
|
"svc/.env"
|
||||||
|
"svc/mail.env"
|
||||||
|
"svc/fonts"
|
||||||
|
"svc/public"
|
||||||
|
)
|
||||||
|
|
||||||
|
log_step() {
|
||||||
|
echo "== $1 =="
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_runtime_files() {
|
||||||
|
RUNTIME_BACKUP_DIR="$(mktemp -d /tmp/bssapp-runtime.XXXXXX)"
|
||||||
|
|
||||||
|
for rel in "${RUNTIME_PRESERVE_FILES[@]}"; do
|
||||||
|
src="$APP_DIR/$rel"
|
||||||
|
dst="$RUNTIME_BACKUP_DIR/$rel"
|
||||||
|
if [[ -e "$src" ]]; then
|
||||||
|
mkdir -p "$(dirname "$dst")"
|
||||||
|
cp -a "$src" "$dst"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_runtime_files() {
|
||||||
|
[[ -n "$RUNTIME_BACKUP_DIR" && -d "$RUNTIME_BACKUP_DIR" ]] || return 0
|
||||||
|
find "$RUNTIME_BACKUP_DIR" -mindepth 1 -print -quit | grep -q . || return 0
|
||||||
|
cp -a "$RUNTIME_BACKUP_DIR/." "$APP_DIR/"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_runtime_backup() {
|
||||||
|
if [[ -n "$RUNTIME_BACKUP_DIR" && -d "$RUNTIME_BACKUP_DIR" ]]; then
|
||||||
|
rm -rf "$RUNTIME_BACKUP_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_runtime_env_files() {
|
||||||
|
[[ -f "$APP_DIR/.env" ]] || touch "$APP_DIR/.env"
|
||||||
|
[[ -f "$APP_DIR/mail.env" ]] || touch "$APP_DIR/mail.env"
|
||||||
|
[[ -f "$APP_DIR/svc/.env" ]] || touch "$APP_DIR/svc/.env"
|
||||||
|
[[ -f "$APP_DIR/svc/mail.env" ]] || touch "$APP_DIR/svc/mail.env"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_pdf_fonts() {
|
||||||
|
local font_dir="$APP_DIR/svc/fonts"
|
||||||
|
local sys_font_dir="/usr/share/fonts/truetype/dejavu"
|
||||||
|
|
||||||
|
mkdir -p "$font_dir"
|
||||||
|
|
||||||
|
if [[ ! -f "$font_dir/DejaVuSans.ttf" && -f "$sys_font_dir/DejaVuSans.ttf" ]]; then
|
||||||
|
cp -a "$sys_font_dir/DejaVuSans.ttf" "$font_dir/DejaVuSans.ttf"
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$font_dir/DejaVuSans-Bold.ttf" && -f "$sys_font_dir/DejaVuSans-Bold.ttf" ]]; then
|
||||||
|
cp -a "$sys_font_dir/DejaVuSans-Bold.ttf" "$font_dir/DejaVuSans-Bold.ttf"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$font_dir/DejaVuSans.ttf" || ! -f "$font_dir/DejaVuSans-Bold.ttf" ]]; then
|
||||||
|
echo "ERROR: Required PDF fonts missing in $font_dir"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_ui_permissions() {
|
||||||
|
local ui_root="$APP_DIR/ui/dist/spa"
|
||||||
|
|
||||||
|
if [[ ! -d "$ui_root" ]]; then
|
||||||
|
echo "ERROR: UI build output not found at $ui_root"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod 755 /opt "$APP_DIR" "$APP_DIR/ui" "$APP_DIR/ui/dist" "$ui_root"
|
||||||
|
find "$ui_root" -type d -exec chmod 755 {} \;
|
||||||
|
find "$ui_root" -type f -exec chmod 644 {} \;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_ui_readable_by_nginx() {
|
||||||
|
local ui_index="$APP_DIR/ui/dist/spa/index.html"
|
||||||
|
|
||||||
|
if [[ ! -f "$ui_index" ]]; then
|
||||||
|
echo "ERROR: UI index not found at $ui_index"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if id -u www-data >/dev/null 2>&1; then
|
||||||
|
if ! su -s /bin/sh -c "test -r '$ui_index'" www-data; then
|
||||||
|
echo "ERROR: www-data cannot read $ui_index"
|
||||||
|
namei -l "$ui_index" || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
build_api_binary() {
|
||||||
|
if ! command -v go >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: go command not found"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export GOPATH="${GOPATH:-/var/cache/bssapp-go}"
|
||||||
|
export GOMODCACHE="${GOMODCACHE:-$GOPATH/pkg/mod}"
|
||||||
|
export GOCACHE="${GOCACHE:-/var/cache/bssapp-go-build}"
|
||||||
|
mkdir -p "$GOPATH" "$GOMODCACHE" "$GOCACHE"
|
||||||
|
|
||||||
|
cd "$APP_DIR/svc"
|
||||||
|
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-w -s" -o "$APP_DIR/svc/bssapp" ./main.go
|
||||||
|
chmod +x "$APP_DIR/svc/bssapp"
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_services() {
|
||||||
|
systemctl daemon-reload || true
|
||||||
|
|
||||||
|
systemctl restart bssapp
|
||||||
|
if ! systemctl is-active --quiet bssapp; then
|
||||||
|
echo "ERROR: bssapp service failed to start"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if systemctl cat nginx >/dev/null 2>&1; then
|
||||||
|
systemctl restart nginx
|
||||||
|
if ! systemctl is-active --quiet nginx; then
|
||||||
|
echo "ERROR: nginx service failed to start"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "WARN: nginx service not found; frontend may be unreachable."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_deploy() {
|
||||||
|
trap cleanup_runtime_backup EXIT
|
||||||
|
|
||||||
|
exec 9>"$LOCK_FILE"
|
||||||
|
if ! flock -n 9; then
|
||||||
|
echo "[$(date '+%F %T')] Deploy already running. Skipping new request."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=============================="
|
||||||
|
echo "[DEPLOY START] $(date '+%F %T')"
|
||||||
|
echo "=============================="
|
||||||
|
|
||||||
|
cd "$APP_DIR"
|
||||||
|
|
||||||
|
log_step "GIT SYNC"
|
||||||
|
backup_runtime_files
|
||||||
|
git fetch origin
|
||||||
|
git reset --hard origin/master
|
||||||
|
git clean -fdx \
|
||||||
|
-e .env \
|
||||||
|
-e mail.env \
|
||||||
|
-e svc/.env \
|
||||||
|
-e svc/mail.env \
|
||||||
|
-e svc/fonts \
|
||||||
|
-e svc/public \
|
||||||
|
-e svc/bssapp
|
||||||
|
restore_runtime_files
|
||||||
|
echo "DEPLOY COMMIT: $(git rev-parse --short HEAD)"
|
||||||
|
|
||||||
|
log_step "BUILD UI"
|
||||||
|
cd "$APP_DIR/ui"
|
||||||
|
npm ci --no-audit --no-fund --include=optional
|
||||||
|
npm i -D --no-audit --no-fund sass-embedded@1.93.2
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
log_step "ENSURE UI PERMISSIONS"
|
||||||
|
ensure_ui_permissions
|
||||||
|
ensure_ui_readable_by_nginx
|
||||||
|
|
||||||
|
log_step "BUILD API"
|
||||||
|
build_api_binary
|
||||||
|
|
||||||
|
log_step "ENSURE ENV FILES"
|
||||||
|
ensure_runtime_env_files
|
||||||
|
|
||||||
|
log_step "ENSURE PDF FONTS"
|
||||||
|
ensure_pdf_fonts
|
||||||
|
|
||||||
|
log_step "RESTART SERVICES"
|
||||||
|
restart_services
|
||||||
|
|
||||||
|
echo "[DEPLOY FINISHED] $(date '+%F %T')"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "--run" ]]; then
|
||||||
|
mkdir -p "$(dirname "$LOG_FILE")"
|
||||||
|
if command -v logger >/dev/null 2>&1; then
|
||||||
|
run_deploy 2>&1 | tee -a "$LOG_FILE" >(logger -t bssapp-deploy -p user.info)
|
||||||
|
exit ${PIPESTATUS[0]}
|
||||||
|
else
|
||||||
|
run_deploy >>"$LOG_FILE" 2>&1
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
nohup /bin/bash "$0" --run </dev/null >/dev/null 2>&1 &
|
||||||
|
exit 0
|
||||||
57
deploy/hooks.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "=============================="
|
|
||||||
echo "🚀 BSSAPP FULL DEPLOY START"
|
|
||||||
echo "=============================="
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# ENV (systemd / webhook fix)
|
|
||||||
# -----------------------------
|
|
||||||
export HOME=/root
|
|
||||||
export PATH=/usr/bin:/usr/local/bin:/bin:$PATH
|
|
||||||
export GOPATH=/root/go
|
|
||||||
export GOMODCACHE=/root/go/pkg/mod
|
|
||||||
|
|
||||||
mkdir -p "$GOPATH" "$GOMODCACHE"
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# PATHS
|
|
||||||
# -----------------------------
|
|
||||||
APP_DIR="/opt/bssapp/svc"
|
|
||||||
UI_DIR="/opt/bssapp/ui"
|
|
||||||
SERVICE="bssapp"
|
|
||||||
|
|
||||||
FONT_DIR="$APP_DIR/fonts"
|
|
||||||
SYS_FONT_DIR="/usr/share/fonts/truetype/dejavu"
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# STOP SERVICE
|
|
||||||
# -----------------------------
|
|
||||||
echo "== STOP SERVICE =="
|
|
||||||
systemctl stop "$SERVICE"
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# UPDATE BACKEND CODE
|
|
||||||
# -----------------------------
|
|
||||||
echo "== UPDATE CODE =="
|
|
||||||
|
|
||||||
cd "$APP_DIR"
|
|
||||||
|
|
||||||
git fetch origin
|
|
||||||
git reset --hard origin/master
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# CHECK FONTS
|
|
||||||
# -----------------------------
|
|
||||||
echo "== CHECK FONTS =="
|
|
||||||
|
|
||||||
mkdir -p "$FONT_DIR"
|
|
||||||
|
|
||||||
if [ ! -f "$FONT_DIR/DejaVuSans.ttf" ]; then
|
|
||||||
cp "$SYS_FONT_DIR/DejaVuSans.ttf" "$FONT_DIR/"
|
|
||||||
|
|
||||||
if [ ! -f "$FONT_DIR/DejaVuSans-Bold.ttf" ]; then
|
|
||||||
cp "$SYS_FONT_DIR/DejaVuSans-Bold.ttf" "$FONT_DIR/"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ Fonts OK"
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# BUILD UI
|
|
||||||
# -----------------------------
|
|
||||||
echo "== BUILD UI =="
|
|
||||||
|
|
||||||
cd "$UI_DIR"
|
|
||||||
|
|
||||||
if ! command -v npm >/dev/null 2>&1; then
|
|
||||||
echo "❌ npm not found!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
npm install
|
|
||||||
npx quasar build
|
|
||||||
|
|
||||||
echo "✅ UI Build Done"
|
|
||||||
# -----------------------------
|
|
||||||
# DEPLOY UI → BACKEND
|
|
||||||
# -----------------------------
|
|
||||||
echo "== DEPLOY UI =="
|
|
||||||
|
|
||||||
rm -rf "$APP_DIR/public"/*
|
|
||||||
mkdir -p "$APP_DIR/public"
|
|
||||||
|
|
||||||
cp -r "$UI_DIR/dist/spa/"* "$APP_DIR/public/"
|
|
||||||
|
|
||||||
echo "✅ UI Copied"
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# BUILD BACKEND
|
|
||||||
# -----------------------------
|
|
||||||
echo "== BUILD BACKEND =="
|
|
||||||
|
|
||||||
cd "$APP_DIR"
|
|
||||||
|
|
||||||
go clean -cache -modcache
|
|
||||||
go mod tidy
|
|
||||||
go build -o bssapp
|
|
||||||
|
|
||||||
echo "✅ Backend Build Done"
|
|
||||||
# -----------------------------
|
|
||||||
# START SERVICE
|
|
||||||
# -----------------------------
|
|
||||||
echo "== START SERVICE =="
|
|
||||||
|
|
||||||
systemctl start "$SERVICE"
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# DONE
|
|
||||||
# -----------------------------
|
|
||||||
echo "=============================="
|
|
||||||
echo "✅ DEPLOY FINISHED"
|
|
||||||
echo "=============================="
|
|
||||||
1~#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "=============================="
|
|
||||||
echo "🚀 BSSAPP FULL DEPLOY START"
|
|
||||||
echo "=============================="
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# ENV (systemd / webhook fix)
|
|
||||||
# -----------------------------
|
|
||||||
export HOME=/root
|
|
||||||
export PATH=/usr/bin:/usr/local/bin:/bin:$PATH
|
|
||||||
export GOPATH=/root/go
|
|
||||||
export GOMODCACHE=/root/go/pkg/mod
|
|
||||||
|
|
||||||
mkdir -p "$GOPATH" "$GOMODCACHE"
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# PATHS
|
|
||||||
# -----------------------------
|
|
||||||
APP_DIR="/opt/bssapp/svc"
|
|
||||||
UI_DIR="/opt/bssapp/ui"
|
|
||||||
SERVICE="bssapp"
|
|
||||||
|
|
||||||
FONT_DIR="$APP_DIR/fonts"
|
|
||||||
SYS_FONT_DIR="/usr/share/fonts/truetype/dejavu"
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# STOP SERVICE
|
|
||||||
# -----------------------------
|
|
||||||
echo "== STOP SERVICE =="
|
|
||||||
systemctl stop "$SERVICE"
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# UPDATE BACKEND CODE
|
|
||||||
# -----------------------------
|
|
||||||
echo "== UPDATE CODE =="
|
|
||||||
|
|
||||||
cd "$APP_DIR"
|
|
||||||
|
|
||||||
git fetch origin
|
|
||||||
git reset --hard origin/master
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# CHECK FONTS
|
|
||||||
# -----------------------------
|
|
||||||
echo "== CHECK FONTS =="
|
|
||||||
|
|
||||||
mkdir -p "$FONT_DIR"
|
|
||||||
|
|
||||||
if [ ! -f "$FONT_DIR/DejaVuSans.ttf" ]; then
|
|
||||||
cp "$SYS_FONT_DIR/DejaVuSans.ttf" "$FONT_DIR/"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "$FONT_DIR/DejaVuSans-Bold.ttf" ]; then
|
|
||||||
cp "$SYS_FONT_DIR/DejaVuSans-Bold.ttf" "$FONT_DIR/"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ Fonts OK"
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# BUILD UI
|
|
||||||
# -----------------------------
|
|
||||||
echo "== BUILD UI =="
|
|
||||||
|
|
||||||
cd "$UI_DIR"
|
|
||||||
|
|
||||||
if ! command -v npm >/dev/null 2>&1; then
|
|
||||||
echo "❌ npm not found!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
npm install
|
|
||||||
npx quasar build
|
|
||||||
|
|
||||||
echo "✅ UI Build Done"
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# DEPLOY UI → BACKEND
|
|
||||||
# -----------------------------
|
|
||||||
rm -rf "$APP_DIR/public"/*
|
|
||||||
mkdir -p "$APP_DIR/public"
|
|
||||||
|
|
||||||
cp -r "$UI_DIR/dist/spa/"* "$APP_DIR/public/"
|
|
||||||
|
|
||||||
echo "✅ UI Copied"
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# BUILD BACKEND
|
|
||||||
# -----------------------------
|
|
||||||
echo "== BUILD BACKEND =="
|
|
||||||
|
|
||||||
cd "$APP_DIR"
|
|
||||||
|
|
||||||
go clean -cache -modcache
|
|
||||||
go mod tidy
|
|
||||||
go build -o bssapp
|
|
||||||
|
|
||||||
echo "✅ Backend Build Done"
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# START SERVICE
|
|
||||||
# -----------------------------
|
|
||||||
echo "== START SERVICE =="
|
|
||||||
systemctl start "$SERVICE"
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# DONE
|
|
||||||
# -----------------------------
|
|
||||||
echo "=============================="
|
|
||||||
echo "✅ DEPLOY FINISHED"
|
|
||||||
echo "=============================="
|
|
||||||
27
scripts/sql/add_indexes_order_validate.sql
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/* Indexes for order validate performance */
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM sys.indexes
|
||||||
|
WHERE name = 'IX_trOrderLine_OrderHeader_ItemCode'
|
||||||
|
AND object_id = OBJECT_ID('dbo.trOrderLine')
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
CREATE NONCLUSTERED INDEX IX_trOrderLine_OrderHeader_ItemCode
|
||||||
|
ON dbo.trOrderLine (OrderHeaderID, ItemCode)
|
||||||
|
INCLUDE (ItemTypeCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, LineDescription, SortOrder, OrderLineID);
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM sys.indexes
|
||||||
|
WHERE name = 'IX_prItemVariant_Combo'
|
||||||
|
AND object_id = OBJECT_ID('dbo.prItemVariant')
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
CREATE NONCLUSTERED INDEX IX_prItemVariant_Combo
|
||||||
|
ON dbo.prItemVariant (ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code)
|
||||||
|
INCLUDE (PLU);
|
||||||
|
END
|
||||||
|
GO
|
||||||
33
svc/.env.local
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# ===============================
|
||||||
|
# SECURITY
|
||||||
|
# ===============================
|
||||||
|
JWT_SECRET=bssapp_super_secret_key_1234567890
|
||||||
|
PASSWORD_RESET_SECRET=1dc7d6d52fd0459a8b1f288a6590428e760f54339f8e47beb20db36b6df6070b
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# URLS (PRODUCTION)
|
||||||
|
# ===============================
|
||||||
|
APP_FRONTEND_URL=http://ss.baggi.com.tr/app
|
||||||
|
API_URL=http://46.224.33.150
|
||||||
|
|
||||||
|
# Eğer Nginx ile /api varsa:
|
||||||
|
# API_URL=http://46.224.33.150/api
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# UI
|
||||||
|
# ===============================
|
||||||
|
UI_DIR=/opt/bssapp/ui/dist
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# DATABASES
|
||||||
|
# ===============================
|
||||||
|
POSTGRES_CONN=host=46.224.33.150 port=5432 user=postgres password=tayitkan dbname=baggib2b sslmode=disable
|
||||||
|
MSSQL_CONN=sqlserver://sa:Gil_0150@100.127.186.137:1433?database=BAGGI_V3&encrypt=disable
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# PDF
|
||||||
|
# ===============================
|
||||||
|
PDF_FONT_DIR=/opt/bssapp/svc/fonts
|
||||||
|
API_HOST=0.0.0.0
|
||||||
|
API_PORT=8080
|
||||||
|
|
||||||
@@ -12,12 +12,11 @@ import (
|
|||||||
|
|
||||||
var MssqlDB *sql.DB
|
var MssqlDB *sql.DB
|
||||||
|
|
||||||
|
// ConnectMSSQL MSSQL baglantisini ortam degiskeninden baslatir.
|
||||||
func ConnectMSSQL() {
|
func ConnectMSSQL() {
|
||||||
connString := strings.TrimSpace(os.Getenv("MSSQL_CONN"))
|
connString := strings.TrimSpace(os.Getenv("MSSQL_CONN"))
|
||||||
|
|
||||||
if connString == "" {
|
if connString == "" {
|
||||||
// Fallback
|
log.Fatal("MSSQL_CONN tanımlı değil")
|
||||||
connString = "server=100.127.186.137;user id=sa;password=Gi l_0150;port=1433;database=BAGGI_V3"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
@@ -26,12 +25,11 @@ func ConnectMSSQL() {
|
|||||||
log.Fatal("MSSQL bağlantı hatası:", err)
|
log.Fatal("MSSQL bağlantı hatası:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = MssqlDB.Ping()
|
if err = MssqlDB.Ping(); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatal("MSSQL erişilemiyor:", err)
|
log.Fatal("MSSQL erişilemiyor:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("✅ MSSQL bağlantısı başarılı!")
|
fmt.Println("MSSQL bağlantısı başarılı")
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDB() *sql.DB {
|
func GetDB() *sql.DB {
|
||||||
|
|||||||
@@ -13,14 +13,11 @@ import (
|
|||||||
|
|
||||||
var PgDB *sql.DB
|
var PgDB *sql.DB
|
||||||
|
|
||||||
// ConnectPostgres → PostgreSQL veritabanına bağlanır
|
// ConnectPostgres PostgreSQL veritabanına bağlanır.
|
||||||
func ConnectPostgres() (*sql.DB, error) {
|
func ConnectPostgres() (*sql.DB, error) {
|
||||||
// Bağlantı stringi (istersen .env’den oku)
|
connStr := strings.TrimSpace(os.Getenv("POSTGRES_CONN"))
|
||||||
connStr := os.Getenv("POSTGRES_CONN")
|
|
||||||
if connStr == "" {
|
if connStr == "" {
|
||||||
// fallback → sabit tanımlı bağlantı
|
return nil, fmt.Errorf("POSTGRES_CONN tanımlı değil")
|
||||||
connStr = "host= 46.224.33.150 port=5432 user=postgres password=tayitkan dbname=baggib2b sslmode=disable"
|
|
||||||
//connStr = "host=172.16.0.3 port=5432 user=postgres password=tayitkan dbname=baggib2b sslmode=disable"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := sql.Open("postgres", connStr)
|
db, err := sql.Open("postgres", connStr)
|
||||||
@@ -28,15 +25,13 @@ func ConnectPostgres() (*sql.DB, error) {
|
|||||||
return nil, fmt.Errorf("PostgreSQL bağlantı hatası: %w", err)
|
return nil, fmt.Errorf("PostgreSQL bağlantı hatası: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// =======================================================
|
// Bağlantı havuzu ayarları (audit log uyumlu).
|
||||||
// 🔹 BAĞLANTI HAVUZU (AUDIT LOG UYUMLU)
|
db.SetMaxOpenConns(30)
|
||||||
// =======================================================
|
|
||||||
db.SetMaxOpenConns(30) // audit + api paralel çalışsın
|
|
||||||
db.SetMaxIdleConns(10)
|
db.SetMaxIdleConns(10)
|
||||||
db.SetConnMaxLifetime(30 * time.Minute)
|
db.SetConnMaxLifetime(30 * time.Minute)
|
||||||
db.SetConnMaxIdleTime(5 * time.Minute) // 🔥 uzun idle audit bağlantılarını kapat
|
db.SetConnMaxIdleTime(5 * time.Minute)
|
||||||
|
|
||||||
// 🔹 Test et
|
// Bağlantıyı test et.
|
||||||
if err = db.Ping(); err != nil {
|
if err = db.Ping(); err != nil {
|
||||||
// Some managed PostgreSQL servers require TLS. If the current DSN uses
|
// Some managed PostgreSQL servers require TLS. If the current DSN uses
|
||||||
// sslmode=disable and server rejects with "no encryption", retry once
|
// sslmode=disable and server rejects with "no encryption", retry once
|
||||||
@@ -45,7 +40,7 @@ func ConnectPostgres() (*sql.DB, error) {
|
|||||||
strings.Contains(err.Error(), "no encryption") &&
|
strings.Contains(err.Error(), "no encryption") &&
|
||||||
strings.Contains(strings.ToLower(connStr), "sslmode=disable") {
|
strings.Contains(strings.ToLower(connStr), "sslmode=disable") {
|
||||||
secureConnStr := strings.Replace(connStr, "sslmode=disable", "sslmode=require", 1)
|
secureConnStr := strings.Replace(connStr, "sslmode=disable", "sslmode=require", 1)
|
||||||
log.Println("⚠️ PostgreSQL requires TLS, retrying with sslmode=require")
|
log.Println("PostgreSQL TLS gerektiriyor, sslmode=require ile tekrar deneniyor")
|
||||||
|
|
||||||
_ = db.Close()
|
_ = db.Close()
|
||||||
db, err = sql.Open("postgres", secureConnStr)
|
db, err = sql.Open("postgres", secureConnStr)
|
||||||
@@ -66,13 +61,12 @@ func ConnectPostgres() (*sql.DB, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("✅ PostgreSQL bağlantısı başarılı!")
|
log.Println("PostgreSQL bağlantısı başarılı")
|
||||||
PgDB = db
|
PgDB = db
|
||||||
return db, nil
|
return db, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPostgresUsers → test amaçlı ilk 5 kullanıcıyı listeler
|
// GetPostgresUsers test amaçlı ilk 5 kullanıcıyı listeler.
|
||||||
func GetPostgresUsers(db *sql.DB) error {
|
func GetPostgresUsers(db *sql.DB) error {
|
||||||
query := `SELECT id, code, email FROM mk_dfusr ORDER BY id LIMIT 5`
|
query := `SELECT id, code, email FROM mk_dfusr ORDER BY id LIMIT 5`
|
||||||
rows, err := db.Query(query)
|
rows, err := db.Query(query)
|
||||||
@@ -81,14 +75,14 @@ func GetPostgresUsers(db *sql.DB) error {
|
|||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
fmt.Println("📋 İlk 5 PostgreSQL kullanıcısı:")
|
fmt.Println("İlk 5 PostgreSQL kullanıcısı:")
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var id int
|
var id int
|
||||||
var code, email string
|
var code, email string
|
||||||
if err := rows.Scan(&id, &code, &email); err != nil {
|
if err := rows.Scan(&id, &code, &email); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf(" ➜ ID: %-4d | USER: %-20s | EMAIL: %s\n", id, code, email)
|
fmt.Printf(" -> ID: %-4d | USER: %-20s | EMAIL: %s\n", id, code, email)
|
||||||
}
|
}
|
||||||
|
|
||||||
return rows.Err()
|
return rows.Err()
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ func (m *GraphMailer) SendPasswordResetMail(toEmail string, resetURL string) err
|
|||||||
<p>Merhaba,</p>
|
<p>Merhaba,</p>
|
||||||
<p>Parolanızı sıfırlamak için aşağıdaki bağlantıya tıklayın:</p>
|
<p>Parolanızı sıfırlamak için aşağıdaki bağlantıya tıklayın:</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="%s">%s</a>
|
<a href="%s">Parolayı sıfırla</a>
|
||||||
</p>
|
</p>
|
||||||
|
<p style="font-size:12px;color:#666;">Bağlantı: %s</p>
|
||||||
<p>Bu bağlantı <strong>30 dakika</strong> geçerlidir ve tek kullanımlıktır.</p>
|
<p>Bu bağlantı <strong>30 dakika</strong> geçerlidir ve tek kullanımlıktır.</p>
|
||||||
`, resetURL, resetURL)
|
`, resetURL, resetURL)
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,28 @@ package security
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BuildResetURL(token string) string {
|
func BuildResetURL(token string) string {
|
||||||
base := os.Getenv("FRONTEND_URL")
|
base := os.Getenv("APP_FRONTEND_URL")
|
||||||
|
if base == "" {
|
||||||
|
base = os.Getenv("FRONTEND_URL")
|
||||||
|
}
|
||||||
if base == "" {
|
if base == "" {
|
||||||
base = "http://localhost:9000"
|
base = "http://localhost:9000"
|
||||||
}
|
}
|
||||||
|
base = strings.TrimRight(base, "/")
|
||||||
|
// If base already points to password-reset, just append token if needed.
|
||||||
|
if strings.Contains(base, "/password-reset") {
|
||||||
|
if strings.HasSuffix(base, "/password-reset") || strings.HasSuffix(base, "/password-reset/") ||
|
||||||
|
strings.HasSuffix(base, "/#/password-reset") || strings.HasSuffix(base, "/#/password-reset/") {
|
||||||
|
return strings.TrimRight(base, "/") + "/" + token
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
if strings.Contains(base, "#") {
|
||||||
return base + "/password-reset/" + token
|
return base + "/password-reset/" + token
|
||||||
|
}
|
||||||
|
return base + "/#/password-reset/" + token
|
||||||
}
|
}
|
||||||
|
|||||||
68
svc/main.go
@@ -207,24 +207,41 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
routes.AuthRefreshHandler(pgDB),
|
routes.AuthRefreshHandler(pgDB),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Password reset flow (public)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/password/forgot", "POST",
|
||||||
|
"auth", "update",
|
||||||
|
routes.ForgotPasswordHandler(pgDB, ml),
|
||||||
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/password/reset/validate/{token}", "GET",
|
||||||
|
"auth", "view",
|
||||||
|
routes.ValidatePasswordResetTokenHandler(pgDB),
|
||||||
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/password/reset", "POST",
|
||||||
|
"auth", "update",
|
||||||
|
routes.CompletePasswordResetHandler(pgDB),
|
||||||
|
)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// SYSTEM
|
// SYSTEM
|
||||||
// ============================================================
|
// ============================================================
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/password/change", "POST",
|
"/api/password/change", "POST",
|
||||||
"system", "update",
|
"auth", "update",
|
||||||
wrapV3(http.HandlerFunc(routes.FirstPasswordChangeHandler(pgDB))),
|
wrapV3(http.HandlerFunc(routes.FirstPasswordChangeHandler(pgDB))),
|
||||||
)
|
)
|
||||||
|
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/activity-logs", "GET",
|
"/api/activity-logs", "GET",
|
||||||
"user", "view",
|
"system", "read",
|
||||||
wrapV3(routes.AdminActivityLogsHandler(pgDB)),
|
wrapV3(routes.AdminActivityLogsHandler(pgDB)),
|
||||||
)
|
)
|
||||||
|
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/test-mail", "POST",
|
"/api/test-mail", "POST",
|
||||||
"user", "insert",
|
"system", "update",
|
||||||
wrapV3(routes.TestMailHandler(ml)),
|
wrapV3(routes.TestMailHandler(ml)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -235,12 +252,12 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
|
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
rolePerm, "GET",
|
rolePerm, "GET",
|
||||||
"user", "update",
|
"system", "update",
|
||||||
wrapV3(routes.GetRolePermissionMatrix(pgDB)),
|
wrapV3(routes.GetRolePermissionMatrix(pgDB)),
|
||||||
)
|
)
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
rolePerm, "POST",
|
rolePerm, "POST",
|
||||||
"user", "update",
|
"system", "update",
|
||||||
wrapV3(routes.SaveRolePermissionMatrix(pgDB)),
|
wrapV3(routes.SaveRolePermissionMatrix(pgDB)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -248,12 +265,12 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
|
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
userPerm, "GET",
|
userPerm, "GET",
|
||||||
"user", "update",
|
"system", "update",
|
||||||
wrapV3(routes.GetUserPermissionsHandler(pgDB)),
|
wrapV3(routes.GetUserPermissionsHandler(pgDB)),
|
||||||
)
|
)
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
userPerm, "POST",
|
userPerm, "POST",
|
||||||
"user", "update",
|
"system", "update",
|
||||||
wrapV3(routes.SaveUserPermissionsHandler(pgDB)),
|
wrapV3(routes.SaveUserPermissionsHandler(pgDB)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -286,17 +303,17 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
|
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/role-dept-permissions/list", "GET",
|
"/api/role-dept-permissions/list", "GET",
|
||||||
"user", "update",
|
"system", "update",
|
||||||
wrapV3(http.HandlerFunc(rdHandler.List)),
|
wrapV3(http.HandlerFunc(rdHandler.List)),
|
||||||
)
|
)
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
rdPerm, "GET",
|
rdPerm, "GET",
|
||||||
"user", "update",
|
"system", "update",
|
||||||
wrapV3(http.HandlerFunc(rdHandler.Get)),
|
wrapV3(http.HandlerFunc(rdHandler.Get)),
|
||||||
)
|
)
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
rdPerm, "POST",
|
rdPerm, "POST",
|
||||||
"user", "update",
|
"system", "update",
|
||||||
wrapV3(http.HandlerFunc(rdHandler.Save)),
|
wrapV3(http.HandlerFunc(rdHandler.Save)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -439,6 +456,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
{"/api/order/update", "POST", "update", http.HandlerFunc(routes.UpdateOrderHandler)},
|
{"/api/order/update", "POST", "update", http.HandlerFunc(routes.UpdateOrderHandler)},
|
||||||
{"/api/order/get/{id}", "GET", "view", routes.GetOrderByIDHandler(mssql)},
|
{"/api/order/get/{id}", "GET", "view", routes.GetOrderByIDHandler(mssql)},
|
||||||
{"/api/orders/list", "GET", "view", routes.OrderListRoute(mssql)},
|
{"/api/orders/list", "GET", "view", routes.OrderListRoute(mssql)},
|
||||||
|
{"/api/orders/production-list", "GET", "update", routes.OrderProductionListRoute(mssql)},
|
||||||
|
{"/api/orders/production-items/{id}", "GET", "view", routes.OrderProductionItemsRoute(mssql)},
|
||||||
|
{"/api/orders/production-items/{id}/insert-missing", "POST", "update", routes.OrderProductionInsertMissingRoute(mssql)},
|
||||||
|
{"/api/orders/production-items/{id}/validate", "POST", "update", routes.OrderProductionValidateRoute(mssql)},
|
||||||
|
{"/api/orders/production-items/{id}/apply", "POST", "update", routes.OrderProductionApplyRoute(mssql)},
|
||||||
{"/api/orders/close-ready", "GET", "update", routes.OrderCloseReadyListRoute(mssql)},
|
{"/api/orders/close-ready", "GET", "update", routes.OrderCloseReadyListRoute(mssql)},
|
||||||
{"/api/orders/bulk-close", "POST", "update", routes.OrderBulkCloseRoute(mssql)},
|
{"/api/orders/bulk-close", "POST", "update", routes.OrderBulkCloseRoute(mssql)},
|
||||||
{"/api/orders/export", "GET", "export", routes.OrderListExcelRoute(mssql)},
|
{"/api/orders/export", "GET", "export", routes.OrderListExcelRoute(mssql)},
|
||||||
@@ -616,8 +638,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) {
|
||||||
@@ -677,12 +712,17 @@ func uiRootDir() string {
|
|||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates := []string{"../ui/dist", "./ui/dist"}
|
candidates := []string{
|
||||||
|
"../ui/dist/spa",
|
||||||
|
"./ui/dist/spa",
|
||||||
|
"../ui/dist",
|
||||||
|
"./ui/dist",
|
||||||
|
}
|
||||||
for _, d := range candidates {
|
for _, d := range candidates {
|
||||||
if fi, err := os.Stat(d); err == nil && fi.IsDir() {
|
if fi, err := os.Stat(d); err == nil && fi.IsDir() {
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "../ui/dist"
|
return "../ui/dist/spa"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -897,6 +897,14 @@ func AuthzGuardByRoute(pg *sql.DB) func(http.Handler) http.Handler {
|
|||||||
return
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// 3️⃣ ROUTE LOOKUP (path + method)
|
// 3️⃣ ROUTE LOOKUP (path + method)
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type OrderList struct {
|
|||||||
// ℹ️ Sipariş Durumu
|
// ℹ️ Sipariş Durumu
|
||||||
CreditableConfirmedDate string `json:"CreditableConfirmedDate"`
|
CreditableConfirmedDate string `json:"CreditableConfirmedDate"`
|
||||||
IsCreditableConfirmed bool `json:"IsCreditableConfirmed"`
|
IsCreditableConfirmed bool `json:"IsCreditableConfirmed"`
|
||||||
|
HasUretimUrunu bool `json:"HasUretimUrunu"`
|
||||||
|
|
||||||
// 💱 Para Birimi
|
// 💱 Para Birimi
|
||||||
DocCurrencyCode string `json:"DocCurrencyCode"`
|
DocCurrencyCode string `json:"DocCurrencyCode"`
|
||||||
|
|||||||
25
svc/models/orderproductionitem.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// 📌 OrderProductionItem — U ile başlayan ürün satırı
|
||||||
|
// ========================================================
|
||||||
|
type OrderProductionItem struct {
|
||||||
|
OrderHeaderID string `json:"OrderHeaderID"`
|
||||||
|
OrderLineID string `json:"OrderLineID"`
|
||||||
|
|
||||||
|
ItemTypeCode int16 `json:"ItemTypeCode"`
|
||||||
|
OldDim1 string `json:"OldDim1"`
|
||||||
|
OldDim3 string `json:"OldDim3"`
|
||||||
|
|
||||||
|
OldItemCode string `json:"OldItemCode"`
|
||||||
|
OldColor string `json:"OldColor"`
|
||||||
|
OldDim2 string `json:"OldDim2"`
|
||||||
|
OldDesc string `json:"OldDesc"`
|
||||||
|
|
||||||
|
NewItemCode string `json:"NewItemCode"`
|
||||||
|
NewColor string `json:"NewColor"`
|
||||||
|
NewDim2 string `json:"NewDim2"`
|
||||||
|
NewDesc string `json:"NewDesc"`
|
||||||
|
|
||||||
|
IsVariantMissing bool `json:"IsVariantMissing"`
|
||||||
|
}
|
||||||
24
svc/models/orderproductionupdate.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type OrderProductionUpdateLine struct {
|
||||||
|
OrderLineID string `json:"OrderLineID"`
|
||||||
|
NewItemCode string `json:"NewItemCode"`
|
||||||
|
NewColor string `json:"NewColor"`
|
||||||
|
NewDim2 string `json:"NewDim2"`
|
||||||
|
NewDesc string `json:"NewDesc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrderProductionUpdatePayload struct {
|
||||||
|
Lines []OrderProductionUpdateLine `json:"lines"`
|
||||||
|
InsertMissing bool `json:"insertMissing"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrderProductionMissingVariant struct {
|
||||||
|
OrderLineID string `json:"OrderLineID"`
|
||||||
|
ItemTypeCode int16 `json:"ItemTypeCode"`
|
||||||
|
ItemCode string `json:"ItemCode"`
|
||||||
|
ColorCode string `json:"ColorCode"`
|
||||||
|
ItemDim1Code string `json:"ItemDim1Code"`
|
||||||
|
ItemDim2Code string `json:"ItemDim2Code"`
|
||||||
|
ItemDim3Code string `json:"ItemDim3Code"`
|
||||||
|
}
|
||||||
@@ -101,8 +101,8 @@ SELECT
|
|||||||
L.ItemDim1Code,
|
L.ItemDim1Code,
|
||||||
L.ItemDim2Code,
|
L.ItemDim2Code,
|
||||||
L.Qty1,
|
L.Qty1,
|
||||||
L.Price,
|
ISNULL(CD.Price, 0) AS Price,
|
||||||
L.DocCurrencyCode,
|
ISNULL(CD.CurrencyCode, ISNULL(L.DocCurrencyCode, 'TRY')) AS DocCurrencyCode,
|
||||||
L.DeliveryDate,
|
L.DeliveryDate,
|
||||||
L.LineDescription,
|
L.LineDescription,
|
||||||
P.ProductAtt01Desc,
|
P.ProductAtt01Desc,
|
||||||
@@ -115,6 +115,9 @@ SELECT
|
|||||||
L.VatCode,
|
L.VatCode,
|
||||||
L.VatRate
|
L.VatRate
|
||||||
FROM BAGGI_V3.dbo.trOrderLine AS L
|
FROM BAGGI_V3.dbo.trOrderLine AS L
|
||||||
|
LEFT JOIN BAGGI_V3.dbo.trOrderLineCurrency AS CD WITH (NOLOCK)
|
||||||
|
ON CD.OrderLineID = L.OrderLineID
|
||||||
|
AND CD.CurrencyCode = ISNULL(NULLIF(LTRIM(RTRIM(L.DocCurrencyCode)), ''), 'TRY')
|
||||||
LEFT JOIN ProductFilterWithDescription('TR') AS P
|
LEFT JOIN ProductFilterWithDescription('TR') AS P
|
||||||
ON LTRIM(RTRIM(P.ProductCode)) = LTRIM(RTRIM(L.ItemCode))
|
ON LTRIM(RTRIM(P.ProductCode)) = LTRIM(RTRIM(L.ItemCode))
|
||||||
WHERE L.OrderHeaderID = @p1
|
WHERE L.OrderHeaderID = @p1
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"bssapp-backend/models"
|
"bssapp-backend/models"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -1114,8 +1115,9 @@ func UpdateOrder(header models.OrderHeader, lines []models.OrderDetail, user *mo
|
|||||||
DELETE FROM BAGGI_V3.dbo.trOrderLine
|
DELETE FROM BAGGI_V3.dbo.trOrderLine
|
||||||
WHERE OrderHeaderID=@p1 AND OrderLineID=@p2 AND ISNULL(IsClosed,0)=0
|
WHERE OrderHeaderID=@p1 AND OrderLineID=@p2 AND ISNULL(IsClosed,0)=0
|
||||||
`, header.OrderHeaderID, lineID); err != nil {
|
`, header.OrderHeaderID, lineID); err != nil {
|
||||||
fmt.Printf("[ORDER_UPDATE] hard delete failed, trying soft-close line_id=%s err=%v\n", lineID, err)
|
fmt.Printf("[ORDER_UPDATE] hard delete failed, trying qty-zero soft-close line_id=%s err=%v\n", lineID, err)
|
||||||
|
|
||||||
|
// IsClosed computed olabilir; sadece miktarları sıfırla.
|
||||||
if _, err2 := tx.Exec(`
|
if _, err2 := tx.Exec(`
|
||||||
UPDATE BAGGI_V3.dbo.trOrderLine
|
UPDATE BAGGI_V3.dbo.trOrderLine
|
||||||
SET
|
SET
|
||||||
@@ -1123,12 +1125,11 @@ SET
|
|||||||
Qty2 = 0,
|
Qty2 = 0,
|
||||||
CancelQty1 = 0,
|
CancelQty1 = 0,
|
||||||
CancelQty2 = 0,
|
CancelQty2 = 0,
|
||||||
IsClosed = 1,
|
|
||||||
LastUpdatedUserName = @p1,
|
LastUpdatedUserName = @p1,
|
||||||
LastUpdatedDate = @p2
|
LastUpdatedDate = @p2
|
||||||
WHERE OrderHeaderID=@p3 AND OrderLineID=@p4 AND ISNULL(IsClosed,0)=0
|
WHERE OrderHeaderID=@p3 AND OrderLineID=@p4 AND ISNULL(IsClosed,0)=0
|
||||||
`, v3User, now, header.OrderHeaderID, lineID); err2 != nil {
|
`, v3User, now, header.OrderHeaderID, lineID); err2 != nil {
|
||||||
return fmt.Errorf("line delete failed line_id=%s: %v; soft-close failed: %w", lineID, err, err2)
|
return fmt.Errorf("line delete failed line_id=%s: %v; qty-zero soft-close failed: %w", lineID, err, err2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1155,6 +1156,13 @@ WHERE OrderHeaderID=@p3 AND OrderLineID=@p4 AND ISNULL(IsClosed,0)=0
|
|||||||
color string
|
color string
|
||||||
dim1 string
|
dim1 string
|
||||||
dim2 string
|
dim2 string
|
||||||
|
qty1 float64
|
||||||
|
price float64
|
||||||
|
docCurrency string
|
||||||
|
vatRate float64
|
||||||
|
lineDesc string
|
||||||
|
deliveryDate string
|
||||||
|
plannedDate string
|
||||||
})
|
})
|
||||||
|
|
||||||
rows, err := tx.Query(`
|
rows, err := tx.Query(`
|
||||||
@@ -1164,7 +1172,14 @@ SELECT
|
|||||||
ISNULL(ItemCode,''),
|
ISNULL(ItemCode,''),
|
||||||
ISNULL(ColorCode,''),
|
ISNULL(ColorCode,''),
|
||||||
ISNULL(ItemDim1Code,''),
|
ISNULL(ItemDim1Code,''),
|
||||||
ISNULL(ItemDim2Code,'')
|
ISNULL(ItemDim2Code,''),
|
||||||
|
ISNULL(Qty1, 0),
|
||||||
|
ISNULL(Price, 0),
|
||||||
|
ISNULL(DocCurrencyCode, ''),
|
||||||
|
ISNULL(VatRate, 0),
|
||||||
|
ISNULL(LineDescription, ''),
|
||||||
|
CONVERT(varchar(10), DeliveryDate, 23),
|
||||||
|
CONVERT(varchar(10), PlannedDateOfLading, 23)
|
||||||
FROM BAGGI_V3.dbo.trOrderLine
|
FROM BAGGI_V3.dbo.trOrderLine
|
||||||
WHERE OrderHeaderID=@p1
|
WHERE OrderHeaderID=@p1
|
||||||
`, header.OrderHeaderID)
|
`, header.OrderHeaderID)
|
||||||
@@ -1175,9 +1190,15 @@ WHERE OrderHeaderID=@p1
|
|||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var id, item, color, dim1, dim2 string
|
var id, item, color, dim1, dim2 string
|
||||||
|
var docCurrency, lineDesc, deliveryDate, plannedDate string
|
||||||
|
var qty1, price, vatRate float64
|
||||||
var closed bool
|
var closed bool
|
||||||
|
|
||||||
if err := rows.Scan(&id, &closed, &item, &color, &dim1, &dim2); err != nil {
|
if err := rows.Scan(
|
||||||
|
&id, &closed, &item, &color, &dim1, &dim2,
|
||||||
|
&qty1, &price, &docCurrency, &vatRate, &lineDesc,
|
||||||
|
&deliveryDate, &plannedDate,
|
||||||
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1195,11 +1216,25 @@ WHERE OrderHeaderID=@p1
|
|||||||
color string
|
color string
|
||||||
dim1 string
|
dim1 string
|
||||||
dim2 string
|
dim2 string
|
||||||
|
qty1 float64
|
||||||
|
price float64
|
||||||
|
docCurrency string
|
||||||
|
vatRate float64
|
||||||
|
lineDesc string
|
||||||
|
deliveryDate string
|
||||||
|
plannedDate string
|
||||||
}{
|
}{
|
||||||
item: strings.TrimSpace(item),
|
item: strings.TrimSpace(item),
|
||||||
color: strings.TrimSpace(color),
|
color: strings.TrimSpace(color),
|
||||||
dim1: strings.TrimSpace(dim1),
|
dim1: strings.TrimSpace(dim1),
|
||||||
dim2: strings.TrimSpace(dim2),
|
dim2: strings.TrimSpace(dim2),
|
||||||
|
qty1: qty1,
|
||||||
|
price: price,
|
||||||
|
docCurrency: strings.TrimSpace(docCurrency),
|
||||||
|
vatRate: vatRate,
|
||||||
|
lineDesc: strings.TrimSpace(lineDesc),
|
||||||
|
deliveryDate: strings.TrimSpace(deliveryDate),
|
||||||
|
plannedDate: strings.TrimSpace(plannedDate),
|
||||||
}
|
}
|
||||||
if combo != "" {
|
if combo != "" {
|
||||||
existingOpenCombo[combo] = id
|
existingOpenCombo[combo] = id
|
||||||
@@ -1391,6 +1426,46 @@ WHERE OrderLineID=@p42 AND ISNULL(IsClosed,0)=0`)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Eğer satır DB'deki değerlerle aynıysa, update/upsert yapma (performans)
|
||||||
|
if !isNew && ln.OrderLineID != "" {
|
||||||
|
if meta, ok := existingOpenMeta[ln.OrderLineID]; ok {
|
||||||
|
trim := strings.TrimSpace
|
||||||
|
floatEq := func(a, b float64) bool { return math.Abs(a-b) < 0.0001 }
|
||||||
|
dateOnly := func(nt models.NullTime) string {
|
||||||
|
if nt.Valid && !nt.Time.IsZero() {
|
||||||
|
return nt.Time.Format("2006-01-02")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
plannedStr := ""
|
||||||
|
if ln.PlannedDateOfLading.Valid {
|
||||||
|
plannedStr = strings.TrimSpace(ln.PlannedDateOfLading.String)
|
||||||
|
}
|
||||||
|
deliveryStr := dateOnly(ln.DeliveryDate)
|
||||||
|
|
||||||
|
payloadHasDates := deliveryStr != "" && plannedStr != ""
|
||||||
|
|
||||||
|
if payloadHasDates &&
|
||||||
|
trim(meta.item) == trim(safeNS(ln.ItemCode)) &&
|
||||||
|
trim(meta.color) == trim(safeNS(ln.ColorCode)) &&
|
||||||
|
trim(meta.dim1) == trim(safeNS(ln.ItemDim1Code)) &&
|
||||||
|
trim(meta.dim2) == trim(safeNS(ln.ItemDim2Code)) &&
|
||||||
|
floatEq(meta.qty1, nf0(ln.Qty1)) &&
|
||||||
|
floatEq(meta.price, nf0(ln.Price)) &&
|
||||||
|
trim(meta.docCurrency) == trim(safeNS(ln.DocCurrencyCode)) &&
|
||||||
|
floatEq(meta.vatRate, nf0(ln.VatRate)) &&
|
||||||
|
trim(meta.lineDesc) == trim(safeNS(ln.LineDescription)) &&
|
||||||
|
trim(meta.deliveryDate) == trim(deliveryStr) &&
|
||||||
|
trim(meta.plannedDate) == trim(plannedStr) {
|
||||||
|
|
||||||
|
// Bu satırı "işlendi" say, ama DB yazma yok
|
||||||
|
delete(existingOpen, ln.OrderLineID)
|
||||||
|
delete(existingOpenCombo, comboKey)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Variant guard
|
// Variant guard
|
||||||
if qtyValue(ln.Qty1) > 0 {
|
if qtyValue(ln.Qty1) > 0 {
|
||||||
if err := ValidateItemVariant(tx, ln); err != nil {
|
if err := ValidateItemVariant(tx, ln); err != nil {
|
||||||
|
|||||||
@@ -112,6 +112,16 @@ SELECT
|
|||||||
END AS PackedRatePct,
|
END AS PackedRatePct,
|
||||||
|
|
||||||
ISNULL(h.IsCreditableConfirmed,0) AS IsCreditableConfirmed,
|
ISNULL(h.IsCreditableConfirmed,0) AS IsCreditableConfirmed,
|
||||||
|
CASE
|
||||||
|
WHEN EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.trOrderLine l2
|
||||||
|
WHERE l2.OrderHeaderID = h.OrderHeaderID
|
||||||
|
AND ISNULL(l2.ItemCode,'') LIKE 'U%%'
|
||||||
|
)
|
||||||
|
THEN CAST(1 AS bit)
|
||||||
|
ELSE CAST(0 AS bit)
|
||||||
|
END AS HasUretimUrunu,
|
||||||
ISNULL(h.Description,'') AS Description,
|
ISNULL(h.Description,'') AS Description,
|
||||||
|
|
||||||
usd.Rate AS ExchangeRateUSD
|
usd.Rate AS ExchangeRateUSD
|
||||||
|
|||||||
222
svc/queries/orderproduction_items.go
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
package queries
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"bssapp-backend/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// 📌 GetOrderProductionItems — OrderHeaderID için U ürünleri
|
||||||
|
// ========================================================
|
||||||
|
func GetOrderProductionItems(mssql *sql.DB, orderHeaderID string) (*sql.Rows, error) {
|
||||||
|
return mssql.Query(`
|
||||||
|
SELECT
|
||||||
|
CAST(l.OrderHeaderID AS NVARCHAR(50)) AS OrderHeaderID,
|
||||||
|
CAST(l.OrderLineID AS NVARCHAR(50)) AS OrderLineID,
|
||||||
|
l.ItemTypeCode AS ItemTypeCode,
|
||||||
|
ISNULL(l.ItemDim1Code,'') AS OldDim1,
|
||||||
|
ISNULL(l.ItemDim3Code,'') AS OldDim3,
|
||||||
|
|
||||||
|
ISNULL(l.ItemCode,'') AS OldItemCode,
|
||||||
|
ISNULL(l.ColorCode,'') AS OldColor,
|
||||||
|
ISNULL(l.ItemDim2Code,'') AS OldDim2,
|
||||||
|
ISNULL(l.LineDescription,'') AS OldDesc,
|
||||||
|
|
||||||
|
CAST('' AS NVARCHAR(60)) AS NewItemCode,
|
||||||
|
CAST('' AS NVARCHAR(30)) AS NewColor,
|
||||||
|
CAST('' AS NVARCHAR(30)) AS NewDim2,
|
||||||
|
CAST('' AS NVARCHAR(250)) AS NewDesc,
|
||||||
|
|
||||||
|
CAST(0 AS bit) AS IsVariantMissing
|
||||||
|
FROM dbo.trOrderLine l
|
||||||
|
WHERE l.OrderHeaderID = @p1
|
||||||
|
AND ISNULL(l.ItemCode,'') LIKE 'U%'
|
||||||
|
ORDER BY l.SortOrder, l.OrderLineID
|
||||||
|
`, orderHeaderID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// 📌 InsertMissingProductionVariants — eksik prItemVariant ekler
|
||||||
|
// ========================================================
|
||||||
|
func InsertMissingProductionVariants(mssql *sql.DB, orderHeaderID string, username string) (int64, error) {
|
||||||
|
query := `
|
||||||
|
;WITH Missing AS (
|
||||||
|
SELECT DISTINCT
|
||||||
|
l.ItemTypeCode,
|
||||||
|
l.ItemCode,
|
||||||
|
l.ColorCode,
|
||||||
|
l.ItemDim1Code,
|
||||||
|
l.ItemDim2Code,
|
||||||
|
l.ItemDim3Code
|
||||||
|
FROM dbo.trOrderLine l
|
||||||
|
LEFT JOIN dbo.prItemVariant pv
|
||||||
|
ON pv.ItemTypeCode = l.ItemTypeCode
|
||||||
|
AND pv.ItemCode = l.ItemCode
|
||||||
|
AND pv.ColorCode = l.ColorCode
|
||||||
|
AND ISNULL(pv.ItemDim1Code,'') = ISNULL(l.ItemDim1Code,'')
|
||||||
|
AND ISNULL(pv.ItemDim2Code,'') = ISNULL(l.ItemDim2Code,'')
|
||||||
|
AND ISNULL(pv.ItemDim3Code,'') = ISNULL(l.ItemDim3Code,'')
|
||||||
|
WHERE l.OrderHeaderID = @p1
|
||||||
|
AND ISNULL(l.ItemCode,'') LIKE 'U%'
|
||||||
|
AND pv.ItemCode IS NULL
|
||||||
|
),
|
||||||
|
MaxPlu AS (
|
||||||
|
SELECT ISNULL(MAX(PLU),0) AS BasePlu
|
||||||
|
FROM dbo.prItemVariant WITH (UPDLOCK, HOLDLOCK)
|
||||||
|
)
|
||||||
|
INSERT INTO dbo.prItemVariant (
|
||||||
|
ItemTypeCode,
|
||||||
|
ItemCode,
|
||||||
|
ColorCode,
|
||||||
|
ItemDim1Code,
|
||||||
|
ItemDim2Code,
|
||||||
|
ItemDim3Code,
|
||||||
|
PLU,
|
||||||
|
CreatedUserName,
|
||||||
|
CreatedDate,
|
||||||
|
LastUpdatedUserName,
|
||||||
|
LastUpdatedDate
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
m.ItemTypeCode,
|
||||||
|
m.ItemCode,
|
||||||
|
m.ColorCode,
|
||||||
|
m.ItemDim1Code,
|
||||||
|
m.ItemDim2Code,
|
||||||
|
m.ItemDim3Code,
|
||||||
|
mp.BasePlu + ROW_NUMBER() OVER (ORDER BY m.ItemCode, m.ColorCode, m.ItemDim1Code, m.ItemDim2Code, m.ItemDim3Code),
|
||||||
|
@p2,
|
||||||
|
GETDATE(),
|
||||||
|
@p2,
|
||||||
|
GETDATE()
|
||||||
|
FROM Missing m
|
||||||
|
CROSS JOIN MaxPlu mp;
|
||||||
|
`
|
||||||
|
|
||||||
|
res, err := mssql.Exec(query, orderHeaderID, username)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return res.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// OrderProductionUpdate - variant kontrolu ve guncelleme
|
||||||
|
// ========================================================
|
||||||
|
func GetOrderLineDims(mssql *sql.DB, orderHeaderID string, orderLineID string) (int16, string, string, string, error) {
|
||||||
|
var itemTypeCode int16
|
||||||
|
var dim1 string
|
||||||
|
var dim2 string
|
||||||
|
var dim3 string
|
||||||
|
err := mssql.QueryRow(`
|
||||||
|
SELECT
|
||||||
|
ItemTypeCode,
|
||||||
|
ISNULL(ItemDim1Code,'') AS ItemDim1Code,
|
||||||
|
ISNULL(ItemDim2Code,'') AS ItemDim2Code,
|
||||||
|
ISNULL(ItemDim3Code,'') AS ItemDim3Code
|
||||||
|
FROM dbo.trOrderLine
|
||||||
|
WHERE OrderHeaderID = @p1 AND OrderLineID = @p2
|
||||||
|
`, orderHeaderID, orderLineID).Scan(&itemTypeCode, &dim1, &dim2, &dim3)
|
||||||
|
return itemTypeCode, dim1, dim2, dim3, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func VariantExists(mssql *sql.DB, itemTypeCode int16, itemCode string, colorCode string, dim1 string, dim2 string, dim3 string) (bool, error) {
|
||||||
|
var exists int
|
||||||
|
err := mssql.QueryRow(`
|
||||||
|
SELECT TOP 1 1
|
||||||
|
FROM dbo.prItemVariant
|
||||||
|
WHERE ItemTypeCode = @p1
|
||||||
|
AND ItemCode = @p2
|
||||||
|
AND ColorCode = @p3
|
||||||
|
AND ISNULL(ItemDim1Code,'') = ISNULL(@p4,'')
|
||||||
|
AND ISNULL(ItemDim2Code,'') = ISNULL(@p5,'')
|
||||||
|
AND ISNULL(ItemDim3Code,'') = ISNULL(@p6,'')
|
||||||
|
`, itemTypeCode, itemCode, colorCode, dim1, dim2, dim3).Scan(&exists)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func InsertMissingVariantsTx(tx *sql.Tx, missing []models.OrderProductionMissingVariant, username string) (int64, error) {
|
||||||
|
if len(missing) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var basePlu int64
|
||||||
|
if err := tx.QueryRow(`
|
||||||
|
SELECT ISNULL(MAX(PLU),0) AS BasePlu
|
||||||
|
FROM dbo.prItemVariant WITH (UPDLOCK, HOLDLOCK)
|
||||||
|
`).Scan(&basePlu); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var inserted int64
|
||||||
|
for i, v := range missing {
|
||||||
|
plu := basePlu + int64(i) + 1
|
||||||
|
res, err := tx.Exec(`
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.prItemVariant
|
||||||
|
WHERE ItemTypeCode = @p1
|
||||||
|
AND ItemCode = @p2
|
||||||
|
AND ColorCode = @p3
|
||||||
|
AND ISNULL(ItemDim1Code,'') = ISNULL(@p4,'')
|
||||||
|
AND ISNULL(ItemDim2Code,'') = ISNULL(@p5,'')
|
||||||
|
AND ISNULL(ItemDim3Code,'') = ISNULL(@p6,'')
|
||||||
|
)
|
||||||
|
INSERT INTO dbo.prItemVariant (
|
||||||
|
ItemTypeCode,
|
||||||
|
ItemCode,
|
||||||
|
ColorCode,
|
||||||
|
ItemDim1Code,
|
||||||
|
ItemDim2Code,
|
||||||
|
ItemDim3Code,
|
||||||
|
PLU,
|
||||||
|
CreatedUserName,
|
||||||
|
CreatedDate,
|
||||||
|
LastUpdatedUserName,
|
||||||
|
LastUpdatedDate
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
@p1, @p2, @p3, @p4, @p5, @p6,
|
||||||
|
@p7, @p8, GETDATE(), @p8, GETDATE()
|
||||||
|
);
|
||||||
|
`, v.ItemTypeCode, v.ItemCode, v.ColorCode, v.ItemDim1Code, v.ItemDim2Code, v.ItemDim3Code, plu, username)
|
||||||
|
if err != nil {
|
||||||
|
return inserted, err
|
||||||
|
}
|
||||||
|
if rows, err := res.RowsAffected(); err == nil {
|
||||||
|
inserted += rows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inserted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateOrderLinesTx(tx *sql.Tx, orderHeaderID string, lines []models.OrderProductionUpdateLine, username string) (int64, error) {
|
||||||
|
var updated int64
|
||||||
|
for _, line := range lines {
|
||||||
|
res, err := tx.Exec(`
|
||||||
|
UPDATE dbo.trOrderLine
|
||||||
|
SET
|
||||||
|
ItemCode = @p1,
|
||||||
|
ColorCode = @p2,
|
||||||
|
ItemDim2Code = @p3,
|
||||||
|
LineDescription = COALESCE(NULLIF(@p4,''), LineDescription),
|
||||||
|
LastUpdatedUserName = @p5,
|
||||||
|
LastUpdatedDate = GETDATE()
|
||||||
|
WHERE OrderHeaderID = @p6 AND OrderLineID = @p7
|
||||||
|
`, line.NewItemCode, line.NewColor, line.NewDim2, line.NewDesc, username, orderHeaderID, line.OrderLineID)
|
||||||
|
if err != nil {
|
||||||
|
return updated, err
|
||||||
|
}
|
||||||
|
if rows, err := res.RowsAffected(); err == nil {
|
||||||
|
updated += rows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updated, nil
|
||||||
|
}
|
||||||
269
svc/queries/orderproductionlist.go
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
package queries
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bssapp-backend/auth"
|
||||||
|
"bssapp-backend/internal/authz"
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// 📌 GetOrderProductionList — Üretime verilecek ürün içeren siparişler
|
||||||
|
// ========================================================
|
||||||
|
func GetOrderProductionList(
|
||||||
|
ctx context.Context,
|
||||||
|
mssql *sql.DB,
|
||||||
|
pg *sql.DB,
|
||||||
|
search string,
|
||||||
|
) (*sql.Rows, error) {
|
||||||
|
|
||||||
|
claims, ok := auth.GetClaimsFromContext(ctx)
|
||||||
|
if !ok || claims == nil {
|
||||||
|
return nil, fmt.Errorf("unauthorized: claims not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// 🔐 PIYASA FILTER (ADMIN BYPASS)
|
||||||
|
// ----------------------------------------------------
|
||||||
|
piyasaWhere := "1=1"
|
||||||
|
|
||||||
|
if !claims.IsAdmin() {
|
||||||
|
|
||||||
|
codes, err := authz.GetUserPiyasaCodes(pg, int(claims.ID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("piyasa codes load error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(codes) == 0 {
|
||||||
|
piyasaWhere = "1=0"
|
||||||
|
} else {
|
||||||
|
piyasaWhere = authz.BuildINClause(
|
||||||
|
"UPPER(f2.CustomerAtt01)",
|
||||||
|
codes,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// 📄 BASE QUERY (orderlist.go ile aynı kolonlar)
|
||||||
|
// ----------------------------------------------------
|
||||||
|
baseQuery := fmt.Sprintf(`
|
||||||
|
SELECT
|
||||||
|
CAST(h.OrderHeaderID AS NVARCHAR(50)) AS OrderHeaderID,
|
||||||
|
ISNULL(h.OrderNumber, '') AS OrderNumber,
|
||||||
|
CONVERT(varchar, h.OrderDate, 23) AS OrderDate,
|
||||||
|
|
||||||
|
ISNULL(h.CurrAccCode, '') AS CurrAccCode,
|
||||||
|
ISNULL(ca.CurrAccDescription, '') AS CurrAccDescription,
|
||||||
|
|
||||||
|
ISNULL(mt.AttributeDescription, '') AS MusteriTemsilcisi,
|
||||||
|
ISNULL(py.AttributeDescription, '') AS Piyasa,
|
||||||
|
|
||||||
|
CONVERT(varchar, h.CreditableConfirmedDate,23) AS CreditableConfirmedDate,
|
||||||
|
ISNULL(h.DocCurrencyCode,'TRY') AS DocCurrencyCode,
|
||||||
|
|
||||||
|
ISNULL(l.TotalAmount,0) AS TotalAmount,
|
||||||
|
|
||||||
|
----------------------------------------------------------------
|
||||||
|
-- USD HESABI (TRY / EUR / GBP / USD DESTEKLİ)
|
||||||
|
----------------------------------------------------------------
|
||||||
|
CASE
|
||||||
|
WHEN h.DocCurrencyCode = 'USD'
|
||||||
|
THEN ISNULL(l.TotalAmount,0)
|
||||||
|
|
||||||
|
WHEN h.DocCurrencyCode = 'TRY'
|
||||||
|
AND usd.Rate > 0
|
||||||
|
THEN ISNULL(l.TotalAmount,0) / usd.Rate
|
||||||
|
|
||||||
|
WHEN h.DocCurrencyCode IN ('EUR','GBP')
|
||||||
|
AND cur.Rate > 0
|
||||||
|
AND usd.Rate > 0
|
||||||
|
THEN (ISNULL(l.TotalAmount,0) * cur.Rate) / usd.Rate
|
||||||
|
|
||||||
|
ELSE 0
|
||||||
|
END AS TotalAmountUSD,
|
||||||
|
|
||||||
|
ISNULL(l.PackedAmount,0) AS PackedAmount,
|
||||||
|
|
||||||
|
CASE
|
||||||
|
WHEN h.DocCurrencyCode = 'USD'
|
||||||
|
THEN ISNULL(l.PackedAmount,0)
|
||||||
|
|
||||||
|
WHEN h.DocCurrencyCode = 'TRY'
|
||||||
|
AND usd.Rate > 0
|
||||||
|
THEN ISNULL(l.PackedTRY,0) / usd.Rate
|
||||||
|
|
||||||
|
WHEN cur.Rate > 0
|
||||||
|
AND usd.Rate > 0
|
||||||
|
THEN (ISNULL(l.PackedAmount,0) * cur.Rate) / usd.Rate
|
||||||
|
|
||||||
|
ELSE 0
|
||||||
|
END AS PackedUSD,
|
||||||
|
|
||||||
|
CASE
|
||||||
|
WHEN ISNULL(l.TotalAmount,0) > 0
|
||||||
|
THEN (ISNULL(l.PackedAmount,0) * 100.0) / NULLIF(l.TotalAmount,0)
|
||||||
|
ELSE 0
|
||||||
|
END AS PackedRatePct,
|
||||||
|
|
||||||
|
ISNULL(h.IsCreditableConfirmed,0) AS IsCreditableConfirmed,
|
||||||
|
CAST(1 AS bit) AS HasUretimUrunu,
|
||||||
|
ISNULL(h.Description,'') AS Description,
|
||||||
|
|
||||||
|
usd.Rate AS ExchangeRateUSD
|
||||||
|
|
||||||
|
FROM dbo.trOrderHeader h
|
||||||
|
|
||||||
|
-- ✅ TOPLAM ARTIK trOrderLineCurrency'den: CurrencyCode = DocCurrencyCode
|
||||||
|
JOIN (
|
||||||
|
SELECT
|
||||||
|
l.OrderHeaderID,
|
||||||
|
SUM(ISNULL(c.NetAmount,0)) AS TotalAmount,
|
||||||
|
SUM(
|
||||||
|
CASE
|
||||||
|
WHEN ISNULL(c.CurrencyCode,'') = 'TRY'
|
||||||
|
THEN ISNULL(c.NetAmount,0)
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
) AS TotalTRY,
|
||||||
|
SUM(
|
||||||
|
CASE
|
||||||
|
WHEN ISNULL(l.IsClosed,0) = 1
|
||||||
|
THEN ISNULL(c.NetAmount,0)
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
) AS PackedAmount,
|
||||||
|
SUM(
|
||||||
|
CASE
|
||||||
|
WHEN ISNULL(l.IsClosed,0) = 1
|
||||||
|
AND ISNULL(c.CurrencyCode,'') = 'TRY'
|
||||||
|
THEN ISNULL(c.NetAmount,0)
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
) AS PackedTRY
|
||||||
|
FROM dbo.trOrderLine l
|
||||||
|
JOIN dbo.trOrderHeader h2
|
||||||
|
ON h2.OrderHeaderID = l.OrderHeaderID
|
||||||
|
LEFT JOIN dbo.trOrderLineCurrency c
|
||||||
|
ON c.OrderLineID = l.OrderLineID
|
||||||
|
AND c.CurrencyCode = ISNULL(h2.DocCurrencyCode,'TRY')
|
||||||
|
GROUP BY l.OrderHeaderID
|
||||||
|
) l
|
||||||
|
ON l.OrderHeaderID = h.OrderHeaderID
|
||||||
|
|
||||||
|
LEFT JOIN dbo.cdCurrAccDesc ca
|
||||||
|
ON ca.CurrAccCode = h.CurrAccCode
|
||||||
|
AND ca.LangCode = 'TR'
|
||||||
|
|
||||||
|
-- müşteri temsilcisi + piyasa açıklamaları
|
||||||
|
LEFT JOIN dbo.CustomerAttributesFilter f
|
||||||
|
ON f.CurrAccCode = h.CurrAccCode
|
||||||
|
|
||||||
|
LEFT JOIN dbo.cdCurrAccAttributeDesc mt
|
||||||
|
ON mt.CurrAccTypeCode = 3
|
||||||
|
AND mt.AttributeTypeCode = 2
|
||||||
|
AND mt.AttributeCode = f.CustomerAtt02
|
||||||
|
AND mt.LangCode = 'TR'
|
||||||
|
|
||||||
|
LEFT JOIN dbo.cdCurrAccAttributeDesc py
|
||||||
|
ON py.CurrAccTypeCode = 3
|
||||||
|
AND py.AttributeTypeCode = 1
|
||||||
|
AND py.AttributeCode = f.CustomerAtt01
|
||||||
|
AND py.LangCode = 'TR'
|
||||||
|
|
||||||
|
----------------------------------------------------------------
|
||||||
|
-- USD → TRY
|
||||||
|
----------------------------------------------------------------
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP 1 Rate
|
||||||
|
FROM dbo.AllExchangeRates
|
||||||
|
WHERE CurrencyCode = 'USD'
|
||||||
|
AND RelationCurrencyCode = 'TRY'
|
||||||
|
AND ExchangeTypeCode = 6
|
||||||
|
AND Rate > 0
|
||||||
|
AND Date <= CAST(GETDATE() AS date)
|
||||||
|
ORDER BY Date DESC
|
||||||
|
) usd
|
||||||
|
|
||||||
|
----------------------------------------------------------------
|
||||||
|
-- ORDER PB → TRY
|
||||||
|
----------------------------------------------------------------
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP 1 Rate
|
||||||
|
FROM dbo.AllExchangeRates
|
||||||
|
WHERE CurrencyCode = h.DocCurrencyCode
|
||||||
|
AND RelationCurrencyCode = 'TRY'
|
||||||
|
AND ExchangeTypeCode = 6
|
||||||
|
AND Rate > 0
|
||||||
|
AND Date <= CAST(GETDATE() AS date)
|
||||||
|
ORDER BY Date DESC
|
||||||
|
) cur
|
||||||
|
|
||||||
|
WHERE
|
||||||
|
ISNULL(h.IsCancelOrder,0) = 0
|
||||||
|
AND h.OrderTypeCode = 1
|
||||||
|
AND h.ProcessCode = 'WS'
|
||||||
|
AND h.IsClosed = 0
|
||||||
|
-- 🔐 PIYASA AUTHZ (EXISTS — SAĞLAM YOL)
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.CustomerAttributesFilter f2
|
||||||
|
WHERE f2.CurrAccCode = h.CurrAccCode
|
||||||
|
AND %s
|
||||||
|
)
|
||||||
|
-- ✅ Üretime verilecek ürün filtresi
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.trOrderLine l2
|
||||||
|
WHERE l2.OrderHeaderID = h.OrderHeaderID
|
||||||
|
AND ISNULL(l2.ItemCode,'') LIKE 'U%%'
|
||||||
|
)
|
||||||
|
`, piyasaWhere)
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// 🔍 SEARCH FILTER (CASE + TR SAFE)
|
||||||
|
// ----------------------------------------------------
|
||||||
|
if search != "" {
|
||||||
|
baseQuery += `
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.trOrderHeader h2
|
||||||
|
LEFT JOIN dbo.cdCurrAccDesc ca2
|
||||||
|
ON ca2.CurrAccCode = h2.CurrAccCode
|
||||||
|
AND ca2.LangCode = 'TR'
|
||||||
|
WHERE h2.OrderHeaderID = h.OrderHeaderID
|
||||||
|
AND (
|
||||||
|
LOWER(REPLACE(REPLACE(h2.OrderNumber,'İ','I'),'ı','i'))
|
||||||
|
COLLATE Latin1_General_CI_AI LIKE LOWER(@p1)
|
||||||
|
|
||||||
|
OR LOWER(REPLACE(REPLACE(h2.CurrAccCode,'İ','I'),'ı','i'))
|
||||||
|
COLLATE Latin1_General_CI_AI LIKE LOWER(@p1)
|
||||||
|
|
||||||
|
OR LOWER(REPLACE(REPLACE(ca2.CurrAccDescription,'İ','I'),'ı','i'))
|
||||||
|
COLLATE Latin1_General_CI_AI LIKE LOWER(@p1)
|
||||||
|
|
||||||
|
OR LOWER(REPLACE(REPLACE(h2.Description,'İ','I'),'ı','i'))
|
||||||
|
COLLATE Latin1_General_CI_AI LIKE LOWER(@p1)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// 📌 ORDER
|
||||||
|
// ----------------------------------------------------
|
||||||
|
baseQuery += `
|
||||||
|
ORDER BY h.CreatedDate DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// ▶ EXECUTE
|
||||||
|
// ----------------------------------------------------
|
||||||
|
if search != "" {
|
||||||
|
searchLike := fmt.Sprintf("%%%s%%", search)
|
||||||
|
return mssql.Query(baseQuery, searchLike)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mssql.Query(baseQuery)
|
||||||
|
}
|
||||||
@@ -10,74 +10,135 @@ import (
|
|||||||
|
|
||||||
// Ana tabloyu getiren fonksiyon (Vue header tablosu için)
|
// Ana tabloyu getiren fonksiyon (Vue header tablosu için)
|
||||||
func GetStatements(params models.StatementParams) ([]models.StatementHeader, error) {
|
func GetStatements(params models.StatementParams) ([]models.StatementHeader, error) {
|
||||||
|
|
||||||
// AccountCode normalize: "ZLA0127" → "ZLA 0127"
|
// AccountCode normalize: "ZLA0127" → "ZLA 0127"
|
||||||
if len(params.AccountCode) == 7 && strings.ContainsAny(params.AccountCode, "0123456789") {
|
if len(params.AccountCode) == 7 && strings.ContainsAny(params.AccountCode, "0123456789") {
|
||||||
params.AccountCode = params.AccountCode[:3] + " " + params.AccountCode[3:]
|
params.AccountCode = params.AccountCode[:3] + " " + params.AccountCode[3:]
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(params.LangCode) == "" {
|
||||||
|
params.LangCode = "TR"
|
||||||
|
}
|
||||||
|
|
||||||
// Parislemler []string → '1','2','3'
|
// Parislemler []string → '1','2','3'
|
||||||
parislemFilter := "''"
|
parislemFilter := "''"
|
||||||
if len(params.Parislemler) > 0 {
|
if len(params.Parislemler) > 0 {
|
||||||
quoted := make([]string, len(params.Parislemler))
|
quoted := make([]string, 0, len(params.Parislemler))
|
||||||
for i, v := range params.Parislemler {
|
for _, v := range params.Parislemler {
|
||||||
quoted[i] = fmt.Sprintf("'%s'", v)
|
v = strings.TrimSpace(v)
|
||||||
|
if v == "" {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
quoted = append(quoted, fmt.Sprintf("'%s'", strings.ReplaceAll(v, "'", "''")))
|
||||||
|
}
|
||||||
|
if len(quoted) > 0 {
|
||||||
parislemFilter = strings.Join(quoted, ",")
|
parislemFilter = strings.Join(quoted, ",")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
;WITH Opening AS (
|
;WITH CurrDesc AS (
|
||||||
SELECT
|
SELECT
|
||||||
b.CurrAccCode AS Cari_Kod,
|
CurrAccCode,
|
||||||
b.DocCurrencyCode AS Para_Birimi,
|
MAX(CurrAccDescription) AS CurrAccDescription
|
||||||
SUM(c.Debit - c.Credit) AS Devir_Bakiyesi
|
FROM cdCurrAccDesc
|
||||||
|
WHERE LangCode = @LangCode
|
||||||
|
GROUP BY CurrAccCode
|
||||||
|
),
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
✅ Bu aralıkta hareket var mı?
|
||||||
|
Varsa : Devir = startdate öncesi
|
||||||
|
Yoksa : Devir = enddate dahil (enddate itibariyle bakiye)
|
||||||
|
========================================================= */
|
||||||
|
HasMovement AS (
|
||||||
|
SELECT
|
||||||
|
CASE WHEN EXISTS (
|
||||||
|
SELECT 1
|
||||||
FROM trCurrAccBook b
|
FROM trCurrAccBook b
|
||||||
|
INNER JOIN CurrAccBookATAttributesFilter f
|
||||||
|
ON f.CurrAccBookID = b.CurrAccBookID
|
||||||
|
AND f.ATAtt01 IN (%s)
|
||||||
|
WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%'
|
||||||
|
AND b.DocumentDate BETWEEN @startdate AND @enddate
|
||||||
|
) THEN 1 ELSE 0 END AS HasMov
|
||||||
|
),
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
✅ Opening (Devir) — TEK CARİ KOD ALTINDA KONSOLİDE
|
||||||
|
Cari_Kod = @Carikod (sabit)
|
||||||
|
========================================================= */
|
||||||
|
Opening AS (
|
||||||
|
SELECT
|
||||||
|
@Carikod AS Cari_Kod,
|
||||||
|
b.DocCurrencyCode AS Para_Birimi,
|
||||||
|
SUM(ISNULL(c.Debit,0) - ISNULL(c.Credit,0)) AS Devir_Bakiyesi
|
||||||
|
FROM trCurrAccBook b
|
||||||
|
CROSS JOIN HasMovement hm
|
||||||
|
INNER JOIN CurrAccBookATAttributesFilter f2
|
||||||
|
ON f2.CurrAccBookID = b.CurrAccBookID
|
||||||
|
AND f2.ATAtt01 IN (%s)
|
||||||
LEFT JOIN trCurrAccBookCurrency c
|
LEFT JOIN trCurrAccBookCurrency c
|
||||||
ON c.CurrAccBookID = b.CurrAccBookID
|
ON c.CurrAccBookID = b.CurrAccBookID
|
||||||
AND c.CurrencyCode = b.DocCurrencyCode
|
AND c.CurrencyCode = b.DocCurrencyCode
|
||||||
WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%'
|
WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%'
|
||||||
AND b.DocumentDate < @startdate
|
AND (
|
||||||
AND EXISTS (
|
(hm.HasMov = 1 AND b.DocumentDate < @startdate) -- hareket varsa: klasik devir
|
||||||
SELECT 1
|
OR (hm.HasMov = 0 AND b.DocumentDate <= @enddate) -- hareket yoksa: enddate itibariyle bakiye
|
||||||
FROM CurrAccBookATAttributesFilter f2
|
|
||||||
WHERE f2.CurrAccBookID = b.CurrAccBookID
|
|
||||||
AND f2.ATAtt01 IN (%s)
|
|
||||||
)
|
)
|
||||||
GROUP BY b.CurrAccCode, b.DocCurrencyCode
|
GROUP BY b.DocCurrencyCode
|
||||||
),
|
),
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
✅ Hareketler (Movements) — TEK CARİ KOD ALTINDA KONSOLİDE
|
||||||
|
Cari_Kod = @Carikod (sabit)
|
||||||
|
Running sadece aralıktaki hareketlerden gelir.
|
||||||
|
========================================================= */
|
||||||
Movements AS (
|
Movements AS (
|
||||||
SELECT
|
SELECT
|
||||||
b.CurrAccCode AS Cari_Kod,
|
@Carikod AS Cari_Kod,
|
||||||
d.CurrAccDescription AS Cari_Isim,
|
|
||||||
|
COALESCE(
|
||||||
|
(SELECT TOP 1 cd.CurrAccDescription
|
||||||
|
FROM CurrDesc cd
|
||||||
|
WHERE cd.CurrAccCode = @Carikod),
|
||||||
|
(SELECT TOP 1 cd.CurrAccDescription
|
||||||
|
FROM CurrDesc cd
|
||||||
|
WHERE cd.CurrAccCode LIKE '%%' + @Carikod + '%%'
|
||||||
|
ORDER BY cd.CurrAccCode)
|
||||||
|
) AS Cari_Isim,
|
||||||
|
|
||||||
CONVERT(varchar(10), b.DocumentDate, 23) AS Belge_Tarihi,
|
CONVERT(varchar(10), b.DocumentDate, 23) AS Belge_Tarihi,
|
||||||
CONVERT(varchar(10), b.DueDate, 23) AS Vade_Tarihi,
|
CONVERT(varchar(10), b.DueDate, 23) AS Vade_Tarihi,
|
||||||
|
|
||||||
b.RefNumber AS Belge_No,
|
b.RefNumber AS Belge_No,
|
||||||
b.BaseApplicationCode AS Islem_Tipi,
|
b.BaseApplicationCode AS Islem_Tipi,
|
||||||
b.LineDescription AS Aciklama,
|
b.LineDescription AS Aciklama,
|
||||||
|
|
||||||
b.DocCurrencyCode AS Para_Birimi,
|
b.DocCurrencyCode AS Para_Birimi,
|
||||||
c.Debit AS Borc,
|
|
||||||
c.Credit AS Alacak,
|
ISNULL(c.Debit,0) AS Borc,
|
||||||
SUM(c.Debit - c.Credit)
|
ISNULL(c.Credit,0) AS Alacak,
|
||||||
OVER (PARTITION BY b.CurrAccCode, c.CurrencyCode
|
|
||||||
ORDER BY b.DocumentDate, b.CurrAccBookID) AS Hareket_Bakiyesi,
|
SUM(ISNULL(c.Debit,0) - ISNULL(c.Credit,0))
|
||||||
|
OVER (
|
||||||
|
PARTITION BY b.DocCurrencyCode
|
||||||
|
ORDER BY b.DocumentDate, b.CurrAccBookID
|
||||||
|
) AS Hareket_Bakiyesi,
|
||||||
|
|
||||||
f.ATAtt01 AS Parislemtipi
|
f.ATAtt01 AS Parislemtipi
|
||||||
|
|
||||||
FROM trCurrAccBook b
|
FROM trCurrAccBook b
|
||||||
LEFT JOIN cdCurrAccDesc d
|
INNER JOIN CurrAccBookATAttributesFilter f
|
||||||
ON b.CurrAccCode = d.CurrAccCode
|
ON f.CurrAccBookID = b.CurrAccBookID
|
||||||
|
AND f.ATAtt01 IN (%s)
|
||||||
LEFT JOIN trCurrAccBookCurrency c
|
LEFT JOIN trCurrAccBookCurrency c
|
||||||
ON b.CurrAccBookID = c.CurrAccBookID
|
ON c.CurrAccBookID = b.CurrAccBookID
|
||||||
AND b.DocCurrencyCode = c.CurrencyCode
|
AND c.CurrencyCode = b.DocCurrencyCode
|
||||||
LEFT JOIN CurrAccBookATAttributesFilter f
|
|
||||||
ON b.CurrAccBookID = f.CurrAccBookID
|
|
||||||
WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%'
|
WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%'
|
||||||
AND b.DocumentDate BETWEEN @startdate AND @enddate
|
AND b.DocumentDate BETWEEN @startdate AND @enddate
|
||||||
AND EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM CurrAccBookATAttributesFilter f2
|
|
||||||
WHERE f2.CurrAccBookID = b.CurrAccBookID
|
|
||||||
AND f2.ATAtt01 IN (%s)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
m.Cari_Kod,
|
m.Cari_Kod,
|
||||||
m.Cari_Isim,
|
m.Cari_Isim,
|
||||||
@@ -89,8 +150,12 @@ SELECT
|
|||||||
m.Para_Birimi,
|
m.Para_Birimi,
|
||||||
m.Borc,
|
m.Borc,
|
||||||
m.Alacak,
|
m.Alacak,
|
||||||
|
|
||||||
|
/* ✅ Bakiye = Devir + Aralıktaki Running */
|
||||||
ISNULL(o.Devir_Bakiyesi,0) + m.Hareket_Bakiyesi AS Bakiye,
|
ISNULL(o.Devir_Bakiyesi,0) + m.Hareket_Bakiyesi AS Bakiye,
|
||||||
|
|
||||||
m.Parislemtipi AS Parislemler
|
m.Parislemtipi AS Parislemler
|
||||||
|
|
||||||
FROM Movements m
|
FROM Movements m
|
||||||
LEFT JOIN Opening o
|
LEFT JOIN Opening o
|
||||||
ON o.Cari_Kod = m.Cari_Kod
|
ON o.Cari_Kod = m.Cari_Kod
|
||||||
@@ -98,43 +163,49 @@ LEFT JOIN Opening o
|
|||||||
|
|
||||||
UNION ALL
|
UNION ALL
|
||||||
|
|
||||||
-- Devir satırı
|
/* =========================================================
|
||||||
|
✅ Devir Satırı (kur bazında) — Opening'den gelir
|
||||||
|
Hareket varsa: startdate öncesi
|
||||||
|
Hareket yoksa: enddate itibariyle bakiye
|
||||||
|
========================================================= */
|
||||||
SELECT
|
SELECT
|
||||||
@Carikod AS Cari_Kod,
|
o.Cari_Kod,
|
||||||
MAX(d.CurrAccDescription) AS Cari_Isim,
|
COALESCE(
|
||||||
|
(SELECT TOP 1 cd.CurrAccDescription
|
||||||
|
FROM CurrDesc cd
|
||||||
|
WHERE cd.CurrAccCode = @Carikod),
|
||||||
|
(SELECT TOP 1 cd.CurrAccDescription
|
||||||
|
FROM CurrDesc cd
|
||||||
|
WHERE cd.CurrAccCode LIKE '%%' + @Carikod + '%%'
|
||||||
|
ORDER BY cd.CurrAccCode)
|
||||||
|
) AS Cari_Isim,
|
||||||
|
|
||||||
CONVERT(varchar(10), @startdate, 23) AS Belge_Tarihi,
|
CONVERT(varchar(10), @startdate, 23) AS Belge_Tarihi,
|
||||||
CONVERT(varchar(10), @startdate, 23) AS Vade_Tarihi,
|
CONVERT(varchar(10), @startdate, 23) AS Vade_Tarihi,
|
||||||
|
|
||||||
'Baslangic_devir' AS Belge_No,
|
'Baslangic_devir' AS Belge_No,
|
||||||
'Devir' AS Islem_Tipi,
|
'Devir' AS Islem_Tipi,
|
||||||
'Devir Bakiyesi' AS Aciklama,
|
'Devir Bakiyesi' AS Aciklama,
|
||||||
b.DocCurrencyCode AS Para_Birimi,
|
|
||||||
SUM(c.Debit) AS Borc,
|
|
||||||
SUM(c.Credit) AS Alacak,
|
|
||||||
SUM(c.Debit) - SUM(c.Credit) AS Bakiye,
|
|
||||||
(
|
|
||||||
SELECT STRING_AGG(x.ATAtt01, ',')
|
|
||||||
FROM (
|
|
||||||
SELECT DISTINCT f2.ATAtt01
|
|
||||||
FROM CurrAccBookATAttributesFilter f2
|
|
||||||
INNER JOIN trCurrAccBook bb
|
|
||||||
ON f2.CurrAccBookID = bb.CurrAccBookID
|
|
||||||
WHERE bb.CurrAccCode LIKE '%%' + @Carikod + '%%'
|
|
||||||
AND bb.DocumentDate < @startdate
|
|
||||||
AND f2.ATAtt01 IN (%s)
|
|
||||||
) x
|
|
||||||
) AS Parislemler
|
|
||||||
FROM trCurrAccBook b
|
|
||||||
LEFT JOIN cdCurrAccDesc d
|
|
||||||
ON b.CurrAccCode = d.CurrAccCode
|
|
||||||
LEFT JOIN trCurrAccBookCurrency c
|
|
||||||
ON b.CurrAccBookID = c.CurrAccBookID
|
|
||||||
AND b.DocCurrencyCode = c.CurrencyCode
|
|
||||||
WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%'
|
|
||||||
AND b.DocumentDate < @startdate
|
|
||||||
GROUP BY b.DocCurrencyCode
|
|
||||||
|
|
||||||
ORDER BY Para_Birimi, Belge_Tarihi;
|
o.Para_Birimi,
|
||||||
`, parislemFilter, parislemFilter, parislemFilter)
|
|
||||||
|
CASE WHEN o.Devir_Bakiyesi >= 0 THEN o.Devir_Bakiyesi ELSE 0 END AS Borc,
|
||||||
|
CASE WHEN o.Devir_Bakiyesi < 0 THEN ABS(o.Devir_Bakiyesi) ELSE 0 END AS Alacak,
|
||||||
|
|
||||||
|
o.Devir_Bakiyesi AS Bakiye,
|
||||||
|
|
||||||
|
CAST(NULL AS varchar(32)) AS Parislemler
|
||||||
|
|
||||||
|
FROM Opening o
|
||||||
|
|
||||||
|
ORDER BY
|
||||||
|
Para_Birimi,
|
||||||
|
Belge_Tarihi;
|
||||||
|
`,
|
||||||
|
parislemFilter, // HasMovement
|
||||||
|
parislemFilter, // Opening
|
||||||
|
parislemFilter, // Movements
|
||||||
|
)
|
||||||
|
|
||||||
rows, err := db.MssqlDB.Query(query,
|
rows, err := db.MssqlDB.Query(query,
|
||||||
sql.Named("startdate", params.StartDate),
|
sql.Named("startdate", params.StartDate),
|
||||||
|
|||||||
@@ -1,187 +1,18 @@
|
|||||||
// queries/statements_header_pdf.go
|
|
||||||
package queries
|
package queries
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bssapp-backend/db"
|
|
||||||
"bssapp-backend/models"
|
"bssapp-backend/models"
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// küçük yardımcı: boşlukları temizle, her değeri ayrı tırnakla sar
|
|
||||||
func buildQuotedHList(vals []string) string {
|
|
||||||
var pp []string
|
|
||||||
for _, v := range vals {
|
|
||||||
v = strings.TrimSpace(v)
|
|
||||||
if v != "" {
|
|
||||||
pp = append(pp, fmt.Sprintf("'%s'", v)) // '1','2' gibi
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(pp) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strings.Join(pp, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================ HEADER (Ana Tablo) ============================ */
|
|
||||||
|
|
||||||
func GetStatementsHPDF(accountCode, startDate, endDate string, parislemler []string) ([]models.StatementHeader, []string, error) {
|
func GetStatementsHPDF(accountCode, startDate, endDate string, parislemler []string) ([]models.StatementHeader, []string, error) {
|
||||||
// Account normalize
|
headers, err := getStatementsForPDF(accountCode, startDate, endDate, parislemler)
|
||||||
if len(accountCode) == 7 && strings.ContainsAny(accountCode, "0123456789") {
|
|
||||||
accountCode = accountCode[:3] + " " + accountCode[3:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// IN list parse et
|
|
||||||
inList := buildQuotedHList(parislemler)
|
|
||||||
parislemCond := "''"
|
|
||||||
if inList != "" {
|
|
||||||
parislemCond = inList
|
|
||||||
}
|
|
||||||
|
|
||||||
query := fmt.Sprintf(`
|
|
||||||
;WITH Opening AS (
|
|
||||||
SELECT
|
|
||||||
b.CurrAccCode AS Cari_Kod,
|
|
||||||
b.DocCurrencyCode AS Para_Birimi,
|
|
||||||
SUM(c.Debit - c.Credit) AS Devir_Bakiyesi
|
|
||||||
FROM trCurrAccBook b
|
|
||||||
LEFT JOIN trCurrAccBookCurrency c
|
|
||||||
ON c.CurrAccBookID = b.CurrAccBookID
|
|
||||||
AND c.CurrencyCode = b.DocCurrencyCode
|
|
||||||
WHERE b.CurrAccCode LIKE @Carikod
|
|
||||||
AND b.DocumentDate < @StartDate
|
|
||||||
AND EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM CurrAccBookATAttributesFilter f2
|
|
||||||
WHERE f2.CurrAccBookID = b.CurrAccBookID
|
|
||||||
AND f2.ATAtt01 IN (%s)
|
|
||||||
)
|
|
||||||
GROUP BY b.CurrAccCode, b.DocCurrencyCode
|
|
||||||
),
|
|
||||||
Movements AS (
|
|
||||||
SELECT
|
|
||||||
b.CurrAccCode AS Cari_Kod,
|
|
||||||
d.CurrAccDescription AS Cari_Isim,
|
|
||||||
CONVERT(varchar(10), b.DocumentDate, 23) AS Belge_Tarihi,
|
|
||||||
CONVERT(varchar(10), b.DueDate, 23) AS Vade_Tarihi,
|
|
||||||
b.RefNumber AS Belge_No,
|
|
||||||
b.BaseApplicationCode AS Islem_Tipi,
|
|
||||||
b.LineDescription AS Aciklama,
|
|
||||||
b.DocCurrencyCode AS Para_Birimi,
|
|
||||||
c.Debit AS Borc,
|
|
||||||
c.Credit AS Alacak,
|
|
||||||
SUM(c.Debit - c.Credit)
|
|
||||||
OVER (PARTITION BY b.CurrAccCode, c.CurrencyCode
|
|
||||||
ORDER BY b.DocumentDate, b.CurrAccBookID) AS Hareket_Bakiyesi,
|
|
||||||
f.ATAtt01 AS Parislemler
|
|
||||||
FROM trCurrAccBook b
|
|
||||||
LEFT JOIN cdCurrAccDesc d
|
|
||||||
ON b.CurrAccCode = d.CurrAccCode AND d.LangCode = 'TR'
|
|
||||||
LEFT JOIN trCurrAccBookCurrency c
|
|
||||||
ON b.CurrAccBookID = c.CurrAccBookID
|
|
||||||
AND b.DocCurrencyCode = c.CurrencyCode
|
|
||||||
LEFT JOIN CurrAccBookATAttributesFilter f
|
|
||||||
ON b.CurrAccBookID = f.CurrAccBookID
|
|
||||||
WHERE b.CurrAccCode LIKE @Carikod
|
|
||||||
AND b.DocumentDate BETWEEN @StartDate AND @EndDate
|
|
||||||
AND EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM CurrAccBookATAttributesFilter f2
|
|
||||||
WHERE f2.CurrAccBookID = b.CurrAccBookID
|
|
||||||
AND f2.ATAtt01 IN (%s)
|
|
||||||
)
|
|
||||||
)`, parislemCond, parislemCond)
|
|
||||||
query += fmt.Sprintf(`
|
|
||||||
SELECT
|
|
||||||
m.Cari_Kod,
|
|
||||||
m.Cari_Isim,
|
|
||||||
m.Belge_Tarihi,
|
|
||||||
m.Vade_Tarihi,
|
|
||||||
m.Belge_No,
|
|
||||||
m.Islem_Tipi,
|
|
||||||
m.Aciklama,
|
|
||||||
m.Para_Birimi,
|
|
||||||
m.Borc,
|
|
||||||
m.Alacak,
|
|
||||||
ISNULL(o.Devir_Bakiyesi,0) + m.Hareket_Bakiyesi AS Bakiye,
|
|
||||||
m.Parislemler
|
|
||||||
FROM Movements m
|
|
||||||
LEFT JOIN Opening o
|
|
||||||
ON o.Cari_Kod = m.Cari_Kod
|
|
||||||
AND o.Para_Birimi = m.Para_Birimi
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
-- Devir satırı
|
|
||||||
SELECT
|
|
||||||
@Carikod AS Cari_Kod,
|
|
||||||
MAX(d.CurrAccDescription) AS Cari_Isim,
|
|
||||||
CONVERT(varchar(10), @StartDate, 23) AS Belge_Tarihi,
|
|
||||||
CONVERT(varchar(10), @StartDate, 23) AS Vade_Tarihi,
|
|
||||||
'Baslangic_devir' AS Belge_No,
|
|
||||||
'Devir' AS Islem_Tipi,
|
|
||||||
'Devir Bakiyesi' AS Aciklama,
|
|
||||||
b.DocCurrencyCode AS Para_Birimi,
|
|
||||||
SUM(c.Debit) AS Borc,
|
|
||||||
SUM(c.Credit) AS Alacak,
|
|
||||||
SUM(c.Debit) - SUM(c.Credit) AS Bakiye,
|
|
||||||
(
|
|
||||||
SELECT STRING_AGG(x.ATAtt01, ',')
|
|
||||||
FROM (
|
|
||||||
SELECT DISTINCT f2.ATAtt01
|
|
||||||
FROM CurrAccBookATAttributesFilter f2
|
|
||||||
INNER JOIN trCurrAccBook bb
|
|
||||||
ON f2.CurrAccBookID = bb.CurrAccBookID
|
|
||||||
WHERE bb.CurrAccCode LIKE @Carikod
|
|
||||||
AND bb.DocumentDate < @StartDate
|
|
||||||
AND f2.ATAtt01 IN (%s)
|
|
||||||
) x
|
|
||||||
) AS Parislemler
|
|
||||||
FROM trCurrAccBook b
|
|
||||||
LEFT JOIN cdCurrAccDesc d
|
|
||||||
ON b.CurrAccCode = d.CurrAccCode AND d.LangCode = 'TR'
|
|
||||||
LEFT JOIN trCurrAccBookCurrency c
|
|
||||||
ON b.CurrAccBookID = c.CurrAccBookID
|
|
||||||
AND b.DocCurrencyCode = c.CurrencyCode
|
|
||||||
WHERE b.CurrAccCode LIKE @Carikod
|
|
||||||
AND b.DocumentDate < @StartDate
|
|
||||||
GROUP BY b.DocCurrencyCode
|
|
||||||
|
|
||||||
ORDER BY Para_Birimi, Belge_Tarihi;`, parislemCond)
|
|
||||||
|
|
||||||
rows, err := db.MssqlDB.Query(query,
|
|
||||||
sql.Named("Carikod", "%"+accountCode+"%"),
|
|
||||||
sql.Named("StartDate", startDate),
|
|
||||||
sql.Named("EndDate", endDate),
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("❌ Header sorgu hatası: %v", err)
|
log.Printf("Header query error: %v", err)
|
||||||
return nil, nil, fmt.Errorf("header sorgu hatası: %v", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
var headers []models.StatementHeader
|
|
||||||
var belgeNos []string
|
|
||||||
for rows.Next() {
|
|
||||||
var h models.StatementHeader
|
|
||||||
if err := rows.Scan(
|
|
||||||
&h.CariKod, &h.CariIsim,
|
|
||||||
&h.BelgeTarihi, &h.VadeTarihi,
|
|
||||||
&h.BelgeNo, &h.IslemTipi,
|
|
||||||
&h.Aciklama, &h.ParaBirimi,
|
|
||||||
&h.Borc, &h.Alacak,
|
|
||||||
&h.Bakiye, &h.Parislemler,
|
|
||||||
); err != nil {
|
|
||||||
log.Printf("❌ Header scan hatası: %v", err)
|
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
headers = append(headers, h)
|
|
||||||
if h.BelgeNo != "" {
|
belgeNos := collectBelgeNos(headers)
|
||||||
belgeNos = append(belgeNos, h.BelgeNo)
|
log.Printf("Header rows fetched: %d, belge no count: %d", len(headers), len(belgeNos))
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Printf("✅ Header verileri alındı: %d kayıt, %d belge no", len(headers), len(belgeNos))
|
|
||||||
return headers, belgeNos, nil
|
return headers, belgeNos, nil
|
||||||
}
|
}
|
||||||
|
|||||||
35
svc/queries/statement_pdf_common.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package queries
|
||||||
|
|
||||||
|
import "bssapp-backend/models"
|
||||||
|
|
||||||
|
func getStatementsForPDF(
|
||||||
|
accountCode string,
|
||||||
|
startDate string,
|
||||||
|
endDate string,
|
||||||
|
parislemler []string,
|
||||||
|
) ([]models.StatementHeader, error) {
|
||||||
|
return GetStatements(models.StatementParams{
|
||||||
|
AccountCode: accountCode,
|
||||||
|
StartDate: startDate,
|
||||||
|
EndDate: endDate,
|
||||||
|
LangCode: "TR",
|
||||||
|
Parislemler: parislemler,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectBelgeNos(headers []models.StatementHeader) []string {
|
||||||
|
seen := make(map[string]struct{}, len(headers))
|
||||||
|
out := make([]string, 0, len(headers))
|
||||||
|
for _, h := range headers {
|
||||||
|
no := h.BelgeNo
|
||||||
|
if no == "" || no == "Baslangic_devir" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[no]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[no] = struct{}{}
|
||||||
|
out = append(out, no)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -32,8 +32,16 @@ SELECT
|
|||||||
a.ItemCode AS Urun_Kodu,
|
a.ItemCode AS Urun_Kodu,
|
||||||
a.ColorCode AS Urun_Rengi,
|
a.ColorCode AS Urun_Rengi,
|
||||||
SUM(a.Qty1) AS Toplam_Adet,
|
SUM(a.Qty1) AS Toplam_Adet,
|
||||||
SUM(ABS(a.Doc_Price)) AS Toplam_Fiyat,
|
|
||||||
CAST(SUM(a.Qty1 * ABS(a.Doc_Price)) AS numeric(18,2)) AS Toplam_Tutar
|
CAST(
|
||||||
|
SUM(a.Qty1 * ABS(a.Doc_Price))
|
||||||
|
/ NULLIF(SUM(a.Qty1),0)
|
||||||
|
AS numeric(18,4)) AS Doviz_Fiyat,
|
||||||
|
|
||||||
|
CAST(
|
||||||
|
SUM(a.Qty1 * ABS(a.Doc_Price))
|
||||||
|
AS numeric(18,2)) AS Toplam_Tutar
|
||||||
|
|
||||||
FROM AllInvoicesWithAttributes a
|
FROM AllInvoicesWithAttributes a
|
||||||
LEFT JOIN prItemAttribute AnaGrup
|
LEFT JOIN prItemAttribute AnaGrup
|
||||||
ON a.ItemCode = AnaGrup.ItemCode AND AnaGrup.AttributeTypeCode = 1
|
ON a.ItemCode = AnaGrup.ItemCode AND AnaGrup.AttributeTypeCode = 1
|
||||||
|
|||||||
@@ -10,179 +10,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// küçük yardımcı: boşlukları temizle, her değeri ayrı tırnakla sar
|
|
||||||
func buildQuotedList(vals []string) string {
|
|
||||||
var pp []string
|
|
||||||
for _, v := range vals {
|
|
||||||
v = strings.TrimSpace(v)
|
|
||||||
if v != "" {
|
|
||||||
pp = append(pp, fmt.Sprintf("'%s'", v)) // '1','2' gibi
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(pp) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strings.Join(pp, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================ HEADER (Ana Tablo) ============================ */
|
|
||||||
|
|
||||||
func GetStatementsPDF(accountCode, startDate, endDate string, parislemler []string) ([]models.StatementHeader, []string, error) {
|
func GetStatementsPDF(accountCode, startDate, endDate string, parislemler []string) ([]models.StatementHeader, []string, error) {
|
||||||
// Account normalize
|
headers, err := getStatementsForPDF(accountCode, startDate, endDate, parislemler)
|
||||||
if len(accountCode) == 7 && strings.ContainsAny(accountCode, "0123456789") {
|
|
||||||
accountCode = accountCode[:3] + " " + accountCode[3:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// IN list parse et
|
|
||||||
inList := buildQuotedList(parislemler)
|
|
||||||
parislemCond := "''"
|
|
||||||
if inList != "" {
|
|
||||||
parislemCond = inList
|
|
||||||
}
|
|
||||||
|
|
||||||
query := fmt.Sprintf(`
|
|
||||||
;WITH Opening AS (
|
|
||||||
SELECT
|
|
||||||
b.CurrAccCode AS Cari_Kod,
|
|
||||||
b.DocCurrencyCode AS Para_Birimi,
|
|
||||||
SUM(c.Debit - c.Credit) AS Devir_Bakiyesi
|
|
||||||
FROM trCurrAccBook b
|
|
||||||
LEFT JOIN trCurrAccBookCurrency c
|
|
||||||
ON c.CurrAccBookID = b.CurrAccBookID
|
|
||||||
AND c.CurrencyCode = b.DocCurrencyCode
|
|
||||||
WHERE b.CurrAccCode LIKE @Carikod
|
|
||||||
AND b.DocumentDate < @StartDate
|
|
||||||
AND EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM CurrAccBookATAttributesFilter f2
|
|
||||||
WHERE f2.CurrAccBookID = b.CurrAccBookID
|
|
||||||
AND f2.ATAtt01 IN (%s)
|
|
||||||
)
|
|
||||||
GROUP BY b.CurrAccCode, b.DocCurrencyCode
|
|
||||||
),
|
|
||||||
Movements AS (
|
|
||||||
SELECT
|
|
||||||
b.CurrAccCode AS Cari_Kod,
|
|
||||||
d.CurrAccDescription AS Cari_Isim,
|
|
||||||
CONVERT(varchar(10), b.DocumentDate, 23) AS Belge_Tarihi,
|
|
||||||
CONVERT(varchar(10), b.DueDate, 23) AS Vade_Tarihi,
|
|
||||||
b.RefNumber AS Belge_No,
|
|
||||||
b.BaseApplicationCode AS Islem_Tipi,
|
|
||||||
b.LineDescription AS Aciklama,
|
|
||||||
b.DocCurrencyCode AS Para_Birimi,
|
|
||||||
c.Debit AS Borc,
|
|
||||||
c.Credit AS Alacak,
|
|
||||||
SUM(c.Debit - c.Credit)
|
|
||||||
OVER (PARTITION BY b.CurrAccCode, c.CurrencyCode
|
|
||||||
ORDER BY b.DocumentDate, b.CurrAccBookID) AS Hareket_Bakiyesi,
|
|
||||||
f.ATAtt01 AS Parislemler
|
|
||||||
FROM trCurrAccBook b
|
|
||||||
LEFT JOIN cdCurrAccDesc d
|
|
||||||
ON b.CurrAccCode = d.CurrAccCode AND d.LangCode = 'TR'
|
|
||||||
LEFT JOIN trCurrAccBookCurrency c
|
|
||||||
ON b.CurrAccBookID = c.CurrAccBookID
|
|
||||||
AND b.DocCurrencyCode = c.CurrencyCode
|
|
||||||
LEFT JOIN CurrAccBookATAttributesFilter f
|
|
||||||
ON b.CurrAccBookID = f.CurrAccBookID
|
|
||||||
WHERE b.CurrAccCode LIKE @Carikod
|
|
||||||
AND b.DocumentDate BETWEEN @StartDate AND @EndDate
|
|
||||||
AND EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM CurrAccBookATAttributesFilter f2
|
|
||||||
WHERE f2.CurrAccBookID = b.CurrAccBookID
|
|
||||||
AND f2.ATAtt01 IN (%s)
|
|
||||||
)
|
|
||||||
)`, parislemCond, parislemCond)
|
|
||||||
query += fmt.Sprintf(`
|
|
||||||
SELECT
|
|
||||||
m.Cari_Kod,
|
|
||||||
m.Cari_Isim,
|
|
||||||
m.Belge_Tarihi,
|
|
||||||
m.Vade_Tarihi,
|
|
||||||
m.Belge_No,
|
|
||||||
m.Islem_Tipi,
|
|
||||||
m.Aciklama,
|
|
||||||
m.Para_Birimi,
|
|
||||||
m.Borc,
|
|
||||||
m.Alacak,
|
|
||||||
ISNULL(o.Devir_Bakiyesi,0) + m.Hareket_Bakiyesi AS Bakiye,
|
|
||||||
m.Parislemler
|
|
||||||
FROM Movements m
|
|
||||||
LEFT JOIN Opening o
|
|
||||||
ON o.Cari_Kod = m.Cari_Kod
|
|
||||||
AND o.Para_Birimi = m.Para_Birimi
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
-- Devir satırı
|
|
||||||
SELECT
|
|
||||||
@Carikod AS Cari_Kod,
|
|
||||||
MAX(d.CurrAccDescription) AS Cari_Isim,
|
|
||||||
CONVERT(varchar(10), @StartDate, 23) AS Belge_Tarihi,
|
|
||||||
CONVERT(varchar(10), @StartDate, 23) AS Vade_Tarihi,
|
|
||||||
'Baslangic_devir' AS Belge_No,
|
|
||||||
'Devir' AS Islem_Tipi,
|
|
||||||
'Devir Bakiyesi' AS Aciklama,
|
|
||||||
b.DocCurrencyCode AS Para_Birimi,
|
|
||||||
SUM(c.Debit) AS Borc,
|
|
||||||
SUM(c.Credit) AS Alacak,
|
|
||||||
SUM(c.Debit) - SUM(c.Credit) AS Bakiye,
|
|
||||||
(
|
|
||||||
SELECT STRING_AGG(x.ATAtt01, ',')
|
|
||||||
FROM (
|
|
||||||
SELECT DISTINCT f2.ATAtt01
|
|
||||||
FROM CurrAccBookATAttributesFilter f2
|
|
||||||
INNER JOIN trCurrAccBook bb
|
|
||||||
ON f2.CurrAccBookID = bb.CurrAccBookID
|
|
||||||
WHERE bb.CurrAccCode LIKE @Carikod
|
|
||||||
AND bb.DocumentDate < @StartDate
|
|
||||||
AND f2.ATAtt01 IN (%s)
|
|
||||||
) x
|
|
||||||
) AS Parislemler
|
|
||||||
FROM trCurrAccBook b
|
|
||||||
LEFT JOIN cdCurrAccDesc d
|
|
||||||
ON b.CurrAccCode = d.CurrAccCode AND d.LangCode = 'TR'
|
|
||||||
LEFT JOIN trCurrAccBookCurrency c
|
|
||||||
ON b.CurrAccBookID = c.CurrAccBookID
|
|
||||||
AND b.DocCurrencyCode = c.CurrencyCode
|
|
||||||
WHERE b.CurrAccCode LIKE @Carikod
|
|
||||||
AND b.DocumentDate < @StartDate
|
|
||||||
GROUP BY b.DocCurrencyCode
|
|
||||||
|
|
||||||
ORDER BY Para_Birimi, Belge_Tarihi;`, parislemCond)
|
|
||||||
|
|
||||||
rows, err := db.MssqlDB.Query(query,
|
|
||||||
sql.Named("Carikod", "%"+accountCode+"%"),
|
|
||||||
sql.Named("StartDate", startDate),
|
|
||||||
sql.Named("EndDate", endDate),
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("❌ Header sorgu hatası: %v", err)
|
log.Printf("Header query error: %v", err)
|
||||||
return nil, nil, fmt.Errorf("header sorgu hatası: %v", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
var headers []models.StatementHeader
|
|
||||||
var belgeNos []string
|
|
||||||
for rows.Next() {
|
|
||||||
var h models.StatementHeader
|
|
||||||
if err := rows.Scan(
|
|
||||||
&h.CariKod, &h.CariIsim,
|
|
||||||
&h.BelgeTarihi, &h.VadeTarihi,
|
|
||||||
&h.BelgeNo, &h.IslemTipi,
|
|
||||||
&h.Aciklama, &h.ParaBirimi,
|
|
||||||
&h.Borc, &h.Alacak,
|
|
||||||
&h.Bakiye, &h.Parislemler,
|
|
||||||
); err != nil {
|
|
||||||
log.Printf("❌ Header scan hatası: %v", err)
|
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
headers = append(headers, h)
|
|
||||||
if h.BelgeNo != "" {
|
belgeNos := collectBelgeNos(headers)
|
||||||
belgeNos = append(belgeNos, h.BelgeNo)
|
log.Printf("Header rows fetched: %d, belge no count: %d", len(headers), len(belgeNos))
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Printf("✅ Header verileri alındı: %d kayıt, %d belge no", len(headers), len(belgeNos))
|
|
||||||
return headers, belgeNos, nil
|
return headers, belgeNos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +27,7 @@ ORDER BY Para_Birimi, Belge_Tarihi;`, parislemCond)
|
|||||||
func GetDetailsMapPDF(belgeNos []string, startDate, endDate string) (map[string][]models.StatementDetail, error) {
|
func GetDetailsMapPDF(belgeNos []string, startDate, endDate string) (map[string][]models.StatementDetail, error) {
|
||||||
result := make(map[string][]models.StatementDetail)
|
result := make(map[string][]models.StatementDetail)
|
||||||
if len(belgeNos) == 0 {
|
if len(belgeNos) == 0 {
|
||||||
log.Println("⚠️ GetDetailsMapPDF: belge listesi boş")
|
log.Println("GetDetailsMapPDF: belge listesi bos")
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +55,11 @@ SELECT
|
|||||||
MAX(ISNULL(KisaKarDesc.AttributeDescription, '')) AS Icerik,
|
MAX(ISNULL(KisaKarDesc.AttributeDescription, '')) AS Icerik,
|
||||||
|
|
||||||
a.ItemCode, a.ColorCode,
|
a.ItemCode, a.ColorCode,
|
||||||
SUM(a.Qty1), SUM(ABS(a.Doc_Price)),
|
SUM(a.Qty1),
|
||||||
|
CAST(
|
||||||
|
SUM(a.Qty1 * ABS(a.Doc_Price))
|
||||||
|
/ NULLIF(SUM(a.Qty1), 0)
|
||||||
|
AS numeric(18,4)),
|
||||||
CAST(SUM(a.Qty1 * ABS(a.Doc_Price)) AS numeric(18,2))
|
CAST(SUM(a.Qty1 * ABS(a.Doc_Price)) AS numeric(18,2))
|
||||||
|
|
||||||
FROM AllInvoicesWithAttributes a
|
FROM AllInvoicesWithAttributes a
|
||||||
@@ -258,7 +98,7 @@ LEFT JOIN cdItemAttributeDesc FitDesc
|
|||||||
AND FitTbl.AttributeCode = FitDesc.AttributeCode
|
AND FitTbl.AttributeCode = FitDesc.AttributeCode
|
||||||
AND FitTbl.ItemTypeCode = FitDesc.ItemTypeCode
|
AND FitTbl.ItemTypeCode = FitDesc.ItemTypeCode
|
||||||
|
|
||||||
-- Kısa Karışım
|
-- Kisa Karisim
|
||||||
LEFT JOIN prItemAttribute KisaKar
|
LEFT JOIN prItemAttribute KisaKar
|
||||||
ON a.ItemCode = KisaKar.ItemCode AND KisaKar.AttributeTypeCode = 41
|
ON a.ItemCode = KisaKar.ItemCode AND KisaKar.AttributeTypeCode = 41
|
||||||
LEFT JOIN cdItemAttributeDesc KisaKarDesc
|
LEFT JOIN cdItemAttributeDesc KisaKarDesc
|
||||||
@@ -274,8 +114,8 @@ ORDER BY a.InvoiceNumber, a.ItemCode, a.ColorCode;`, inBelge)
|
|||||||
sql.Named("EndDate", endDate),
|
sql.Named("EndDate", endDate),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("❌ Detay sorgu hatası: %v", err)
|
log.Printf("Detail query error: %v", err)
|
||||||
return nil, fmt.Errorf("detay sorgu hatası: %v", err)
|
return nil, fmt.Errorf("detay sorgu hatasi: %v", err)
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
@@ -295,11 +135,11 @@ ORDER BY a.InvoiceNumber, a.ItemCode, a.ColorCode;`, inBelge)
|
|||||||
&d.ToplamFiyat,
|
&d.ToplamFiyat,
|
||||||
&d.ToplamTutar,
|
&d.ToplamTutar,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.Printf("❌ Detay scan hatası: %v", err)
|
log.Printf("Detail scan error: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result[d.BelgeRefNumarasi] = append(result[d.BelgeRefNumarasi], d)
|
result[d.BelgeRefNumarasi] = append(result[d.BelgeRefNumarasi], d)
|
||||||
}
|
}
|
||||||
log.Printf("✅ Detay verileri alındı: %d belge için detay var", len(result))
|
log.Printf("Detail rows fetched for %d belge", len(result))
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,11 +234,18 @@ func (r *UserRepository) GetLegacyUserForLogin(login string) (*models.User, erro
|
|||||||
COALESCE(u.upass,'') as upass,
|
COALESCE(u.upass,'') as upass,
|
||||||
u.is_active,
|
u.is_active,
|
||||||
COALESCE(u.email,''),
|
COALESCE(u.email,''),
|
||||||
COALESCE(u.dfrole_id,0) as role_id,
|
COALESCE(ru.dfrole_id,0) as role_id,
|
||||||
COALESCE(dr.code,'') as role_code,
|
COALESCE(dr.code,'') as role_code,
|
||||||
COALESCE(u.force_password_change,false)
|
COALESCE(u.force_password_change,false)
|
||||||
FROM dfusr u
|
FROM dfusr u
|
||||||
LEFT JOIN dfrole dr ON dr.id = u.dfrole_id
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT dfrole_id
|
||||||
|
FROM dfrole_usr
|
||||||
|
WHERE dfusr_id = u.id
|
||||||
|
ORDER BY dfrole_id
|
||||||
|
LIMIT 1
|
||||||
|
) ru ON true
|
||||||
|
LEFT JOIN dfrole dr ON dr.id = ru.dfrole_id
|
||||||
WHERE u.is_active = true
|
WHERE u.is_active = true
|
||||||
AND (
|
AND (
|
||||||
LOWER(u.code) = LOWER($1)
|
LOWER(u.code) = LOWER($1)
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func AdminResetPasswordHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
// ---------------------------------------------------
|
// ---------------------------------------------------
|
||||||
// 4️⃣ UPDATE mk_dfusr
|
// 4️⃣ UPDATE mk_dfusr
|
||||||
// ---------------------------------------------------
|
// ---------------------------------------------------
|
||||||
_, err = db.Exec(`
|
res, err := db.Exec(`
|
||||||
UPDATE mk_dfusr
|
UPDATE mk_dfusr
|
||||||
SET
|
SET
|
||||||
password_hash = $1,
|
password_hash = $1,
|
||||||
@@ -77,6 +77,24 @@ func AdminResetPasswordHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
affected, _ := res.RowsAffected()
|
||||||
|
if affected == 0 {
|
||||||
|
_, err = db.Exec(`
|
||||||
|
UPDATE dfusr
|
||||||
|
SET
|
||||||
|
upass = $1,
|
||||||
|
force_password_change = true,
|
||||||
|
last_updated_date = NOW()
|
||||||
|
WHERE id = $2
|
||||||
|
AND is_active = true
|
||||||
|
`, string(hash), userID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "legacy password reset failed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------
|
// ---------------------------------------------------
|
||||||
// 5️⃣ REFRESH TOKEN REVOKE
|
// 5️⃣ REFRESH TOKEN REVOKE
|
||||||
// ---------------------------------------------------
|
// ---------------------------------------------------
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
r.Method,
|
r.Method,
|
||||||
r.URL.Path,
|
r.URL.Path,
|
||||||
)
|
)
|
||||||
http.Error(w, "unauthorized: token missing or invalid", http.StatusUnauthorized)
|
http.Error(w, "yetkisiz: token eksik veya geçersiz", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,14 +39,14 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid payload", http.StatusBadRequest)
|
http.Error(w, "geçersiz istek gövdesi", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
req.CurrentPassword = strings.TrimSpace(req.CurrentPassword)
|
req.CurrentPassword = strings.TrimSpace(req.CurrentPassword)
|
||||||
req.NewPassword = strings.TrimSpace(req.NewPassword)
|
req.NewPassword = strings.TrimSpace(req.NewPassword)
|
||||||
if req.CurrentPassword == "" || req.NewPassword == "" {
|
if req.CurrentPassword == "" || req.NewPassword == "" {
|
||||||
http.Error(w, "password fields required", http.StatusUnprocessableEntity)
|
http.Error(w, "şifre alanları zorunludur", http.StatusUnprocessableEntity)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
claims.ID,
|
claims.ID,
|
||||||
mkErr,
|
mkErr,
|
||||||
)
|
)
|
||||||
http.Error(w, "user lookup failed", http.StatusInternalServerError)
|
http.Error(w, "kullanıcı sorgulama hatası", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,20 +79,30 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
claims.ID,
|
claims.ID,
|
||||||
claims.Username,
|
claims.Username,
|
||||||
)
|
)
|
||||||
http.Error(w, "mevcut sifre hatali", http.StatusUnauthorized)
|
http.Error(w, "mevcut şifre hatalı", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
legacyUser, err = legacyRepo.GetLegacyUserForLogin(claims.Username)
|
legacyUser, err = legacyRepo.GetLegacyUserForLogin(claims.Username)
|
||||||
if err != nil || legacyUser == nil || !legacyUser.IsActive || int64(legacyUser.ID) != claims.ID {
|
if err != nil || legacyUser == nil || !legacyUser.IsActive {
|
||||||
log.Printf(
|
log.Printf(
|
||||||
"FIRST_PASSWORD_CHANGE 401 reason=legacy_user_not_found user_id=%d username=%s err=%v",
|
"FIRST_PASSWORD_CHANGE 401 reason=legacy_user_not_found user_id=%d username=%s err=%v",
|
||||||
claims.ID,
|
claims.ID,
|
||||||
claims.Username,
|
claims.Username,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
http.Error(w, "unauthorized: user not found", http.StatusUnauthorized)
|
http.Error(w, "yetkisiz: kullanıcı bulunamadı", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !hasMkUser && int64(legacyUser.ID) != claims.ID {
|
||||||
|
log.Printf(
|
||||||
|
"FIRST_PASSWORD_CHANGE 401 reason=legacy_id_mismatch user_id=%d legacy_id=%d username=%s",
|
||||||
|
claims.ID,
|
||||||
|
legacyUser.ID,
|
||||||
|
claims.Username,
|
||||||
|
)
|
||||||
|
http.Error(w, "yetkisiz: kullanıcı bulunamadı", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +112,7 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
claims.ID,
|
claims.ID,
|
||||||
claims.Username,
|
claims.Username,
|
||||||
)
|
)
|
||||||
http.Error(w, "mevcut sifre hatali", http.StatusUnauthorized)
|
http.Error(w, "mevcut şifre hatalı", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,13 +127,13 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
bcrypt.DefaultCost,
|
bcrypt.DefaultCost,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "password hash error", http.StatusInternalServerError)
|
http.Error(w, "şifre hash hatası", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := db.Begin()
|
tx, err := db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "transaction error", http.StatusInternalServerError)
|
http.Error(w, "işlem başlatılamadı", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
@@ -146,7 +156,7 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
claims.ID,
|
claims.ID,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
http.Error(w, "password update failed", http.StatusInternalServerError)
|
http.Error(w, "şifre güncellenemedi", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,21 +166,31 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
"FIRST_PASSWORD_CHANGE 500 reason=password_update_no_rows user_id=%d",
|
"FIRST_PASSWORD_CHANGE 500 reason=password_update_no_rows user_id=%d",
|
||||||
claims.ID,
|
claims.ID,
|
||||||
)
|
)
|
||||||
http.Error(w, "password update failed", http.StatusInternalServerError)
|
http.Error(w, "şifre güncellenemedi", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if legacyUser == nil {
|
if legacyUser == nil {
|
||||||
// Defensive fallback, should not happen.
|
// Defensive fallback, should not happen.
|
||||||
legacyUser, err = legacyRepo.GetLegacyUserForLogin(claims.Username)
|
legacyUser, err = legacyRepo.GetLegacyUserForLogin(claims.Username)
|
||||||
if err != nil || legacyUser == nil || int64(legacyUser.ID) != claims.ID {
|
if err != nil || legacyUser == nil {
|
||||||
log.Printf(
|
log.Printf(
|
||||||
"FIRST_PASSWORD_CHANGE 500 reason=legacy_reload_failed user_id=%d username=%s err=%v",
|
"FIRST_PASSWORD_CHANGE 500 reason=legacy_reload_failed user_id=%d username=%s err=%v",
|
||||||
claims.ID,
|
claims.ID,
|
||||||
claims.Username,
|
claims.Username,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
http.Error(w, "legacy user reload failed", http.StatusInternalServerError)
|
http.Error(w, "legacy kullanıcı yeniden yüklenemedi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !hasMkUser && int64(legacyUser.ID) != claims.ID {
|
||||||
|
log.Printf(
|
||||||
|
"FIRST_PASSWORD_CHANGE 500 reason=legacy_reload_id_mismatch user_id=%d legacy_id=%d username=%s",
|
||||||
|
claims.ID,
|
||||||
|
legacyUser.ID,
|
||||||
|
claims.Username,
|
||||||
|
)
|
||||||
|
http.Error(w, "legacy kullanıcı yeniden yüklenemedi", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,7 +242,7 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
claims.Username,
|
claims.Username,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
http.Error(w, "legacy migration failed", http.StatusInternalServerError)
|
http.Error(w, "legacy geçişi başarısız", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +255,7 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
claims.ID,
|
claims.ID,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
http.Error(w, "commit failed", http.StatusInternalServerError)
|
http.Error(w, "işlem tamamlanamadı", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,7 +282,7 @@ func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "token generation failed", http.StatusInternalServerError)
|
http.Error(w, "token üretilemedi", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -136,8 +224,15 @@ func LoginHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
// 3️⃣ LEGACY SESSION (PENDING MIGRATION)
|
// 3️⃣ LEGACY SESSION (PENDING MIGRATION)
|
||||||
// - mk_dfusr migration is completed in /api/password/change
|
// - mk_dfusr migration is completed in /api/password/change
|
||||||
// ==================================================
|
// ==================================================
|
||||||
|
mkID, err := ensureLegacyUserReadyForSession(db, legacyUser)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("LEGACY LOGIN MIGRATION BIND FAILED username=%s err=%v", login, err)
|
||||||
|
http.Error(w, "Giriş yapılamadı", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
mkUser = &models.MkUser{
|
mkUser = &models.MkUser{
|
||||||
ID: int64(legacyUser.ID),
|
ID: mkID,
|
||||||
Username: legacyUser.Username,
|
Username: legacyUser.Username,
|
||||||
Email: legacyUser.Email,
|
Email: legacyUser.Email,
|
||||||
IsActive: legacyUser.IsActive,
|
IsActive: legacyUser.IsActive,
|
||||||
@@ -149,7 +244,7 @@ func LoginHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
auditlog.Write(auditlog.ActivityLog{
|
auditlog.Write(auditlog.ActivityLog{
|
||||||
ActionType: "LEGACY_USER_LOGIN_PENDING_MIGRATION",
|
ActionType: "LEGACY_USER_LOGIN_PENDING_MIGRATION",
|
||||||
ActionCategory: "security",
|
ActionCategory: "security",
|
||||||
Description: "legacy login ok, first password change required",
|
Description: "legacy giriş başarılı, ilk şifre değişikliği gerekli",
|
||||||
IsSuccess: true,
|
IsSuccess: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -208,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{
|
||||||
|
|||||||
@@ -446,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'
|
||||||
@@ -507,8 +507,8 @@ func getOrderLinesFromDB(db *sql.DB, orderID string) ([]OrderLineRaw, error) {
|
|||||||
L.ItemDim1Code,
|
L.ItemDim1Code,
|
||||||
L.ItemDim2Code,
|
L.ItemDim2Code,
|
||||||
L.Qty1,
|
L.Qty1,
|
||||||
L.Price,
|
ISNULL(CD.Price, 0) AS Price,
|
||||||
L.DocCurrencyCode,
|
ISNULL(CD.CurrencyCode, ISNULL(L.DocCurrencyCode, 'TRY')) AS DocCurrencyCode,
|
||||||
L.DeliveryDate,
|
L.DeliveryDate,
|
||||||
L.LineDescription,
|
L.LineDescription,
|
||||||
P.ProductAtt01Desc,
|
P.ProductAtt01Desc,
|
||||||
@@ -521,6 +521,9 @@ func getOrderLinesFromDB(db *sql.DB, orderID string) ([]OrderLineRaw, error) {
|
|||||||
L.VatCode,
|
L.VatCode,
|
||||||
L.VatRate
|
L.VatRate
|
||||||
FROM BAGGI_V3.dbo.trOrderLine AS L
|
FROM BAGGI_V3.dbo.trOrderLine AS L
|
||||||
|
LEFT JOIN BAGGI_V3.dbo.trOrderLineCurrency AS CD WITH (NOLOCK)
|
||||||
|
ON CD.OrderLineID = L.OrderLineID
|
||||||
|
AND CD.CurrencyCode = ISNULL(NULLIF(LTRIM(RTRIM(L.DocCurrencyCode)), ''), 'TRY')
|
||||||
LEFT JOIN ProductFilterWithDescription('TR') AS P
|
LEFT JOIN ProductFilterWithDescription('TR') AS P
|
||||||
ON LTRIM(RTRIM(P.ProductCode)) = LTRIM(RTRIM(L.ItemCode))
|
ON LTRIM(RTRIM(P.ProductCode)) = LTRIM(RTRIM(L.ItemCode))
|
||||||
WHERE L.OrderHeaderID = @p1
|
WHERE L.OrderHeaderID = @p1
|
||||||
@@ -777,8 +780,12 @@ func drawOrderHeader(pdf *gofpdf.Fpdf, h *OrderHeader, showDesc bool) float64 {
|
|||||||
/* ----------------------------------------------------
|
/* ----------------------------------------------------
|
||||||
5) AÇIKLAMA (Varsa)
|
5) AÇIKLAMA (Varsa)
|
||||||
---------------------------------------------------- */
|
---------------------------------------------------- */
|
||||||
if showDesc && strings.TrimSpace(h.Description) != "" {
|
desc := strings.TrimSpace(h.Description)
|
||||||
text := strings.TrimSpace(h.Description)
|
if desc == "" {
|
||||||
|
desc = strings.TrimSpace(h.InternalDesc)
|
||||||
|
}
|
||||||
|
if showDesc && desc != "" {
|
||||||
|
text := desc
|
||||||
|
|
||||||
pdf.SetFont("dejavu", "", 8) // wrap’te kullanılacak font
|
pdf.SetFont("dejavu", "", 8) // wrap’te kullanılacak font
|
||||||
lineH := 4.0
|
lineH := 4.0
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ func OrderListRoute(mssql *sql.DB) http.Handler {
|
|||||||
count := 0
|
count := 0
|
||||||
|
|
||||||
// ==================================================
|
// ==================================================
|
||||||
// 🧠 SCAN — SQL SELECT ile BİRE BİR (17 kolon)
|
// 🧠 SCAN — SQL SELECT ile BİRE BİR (18 kolon)
|
||||||
// ==================================================
|
// ==================================================
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
|
|
||||||
@@ -85,9 +85,10 @@ func OrderListRoute(mssql *sql.DB) http.Handler {
|
|||||||
&o.PackedRatePct, // 14
|
&o.PackedRatePct, // 14
|
||||||
|
|
||||||
&o.IsCreditableConfirmed, // 15
|
&o.IsCreditableConfirmed, // 15
|
||||||
&o.Description, // 16
|
&o.HasUretimUrunu, // 16
|
||||||
|
&o.Description, // 17
|
||||||
|
|
||||||
&o.ExchangeRateUSD, // 17
|
&o.ExchangeRateUSD, // 18
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
294
svc/routes/orderproductionitems.go
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bssapp-backend/auth"
|
||||||
|
"bssapp-backend/models"
|
||||||
|
"bssapp-backend/queries"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 📌 OrderProductionItemsRoute — U ürün satırları
|
||||||
|
// ======================================================
|
||||||
|
func OrderProductionItemsRoute(mssql *sql.DB) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
|
id := mux.Vars(r)["id"]
|
||||||
|
if id == "" {
|
||||||
|
http.Error(w, "OrderHeaderID bulunamadı", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := queries.GetOrderProductionItems(mssql, id)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("❌ SQL sorgu hatası: %v", err)
|
||||||
|
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
list := make([]models.OrderProductionItem, 0, 100)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var o models.OrderProductionItem
|
||||||
|
if err := rows.Scan(
|
||||||
|
&o.OrderHeaderID,
|
||||||
|
&o.OrderLineID,
|
||||||
|
&o.ItemTypeCode,
|
||||||
|
&o.OldDim1,
|
||||||
|
&o.OldDim3,
|
||||||
|
&o.OldItemCode,
|
||||||
|
&o.OldColor,
|
||||||
|
&o.OldDim2,
|
||||||
|
&o.OldDesc,
|
||||||
|
&o.NewItemCode,
|
||||||
|
&o.NewColor,
|
||||||
|
&o.NewDim2,
|
||||||
|
&o.NewDesc,
|
||||||
|
&o.IsVariantMissing,
|
||||||
|
); err != nil {
|
||||||
|
log.Printf("⚠️ SCAN HATASI: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
list = append(list, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
log.Printf("⚠️ rows.Err(): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(list); err != nil {
|
||||||
|
log.Printf("❌ encode error: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 📌 OrderProductionInsertMissingRoute — eksik varyantları ekler
|
||||||
|
// ======================================================
|
||||||
|
func OrderProductionInsertMissingRoute(mssql *sql.DB) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
|
id := mux.Vars(r)["id"]
|
||||||
|
if id == "" {
|
||||||
|
http.Error(w, "OrderHeaderID bulunamadı", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, _ := auth.GetClaimsFromContext(r.Context())
|
||||||
|
username := ""
|
||||||
|
if claims != nil {
|
||||||
|
username = claims.Username
|
||||||
|
}
|
||||||
|
if username == "" {
|
||||||
|
username = "system"
|
||||||
|
}
|
||||||
|
|
||||||
|
affected, err := queries.InsertMissingProductionVariants(mssql, id, username)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("❌ INSERT varyant hatası: %v", err)
|
||||||
|
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := map[string]any{
|
||||||
|
"inserted": affected,
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||||
|
log.Printf("❌ encode error: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// OrderProductionValidateRoute - yeni model varyant kontrolu
|
||||||
|
// ======================================================
|
||||||
|
func OrderProductionValidateRoute(mssql *sql.DB) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
|
id := mux.Vars(r)["id"]
|
||||||
|
if id == "" {
|
||||||
|
http.Error(w, "OrderHeaderID bulunamadi", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload models.OrderProductionUpdatePayload
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||||
|
http.Error(w, "Gecersiz istek", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validateUpdateLines(payload.Lines); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
missing, err := buildMissingVariants(mssql, id, payload.Lines)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("❌ validate error: %v", err)
|
||||||
|
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := map[string]any{
|
||||||
|
"missingCount": len(missing),
|
||||||
|
"missing": missing,
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||||
|
log.Printf("❌ encode error: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// OrderProductionApplyRoute - yeni model varyant guncelleme
|
||||||
|
// ======================================================
|
||||||
|
func OrderProductionApplyRoute(mssql *sql.DB) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
|
id := mux.Vars(r)["id"]
|
||||||
|
if id == "" {
|
||||||
|
http.Error(w, "OrderHeaderID bulunamadi", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload models.OrderProductionUpdatePayload
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||||
|
http.Error(w, "Gecersiz istek", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validateUpdateLines(payload.Lines); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
missing, err := buildMissingVariants(mssql, id, payload.Lines)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("❌ apply validate error: %v", err)
|
||||||
|
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missing) > 0 && !payload.InsertMissing {
|
||||||
|
w.WriteHeader(http.StatusConflict)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"missingCount": len(missing),
|
||||||
|
"missing": missing,
|
||||||
|
"message": "Eksik varyantlar var",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, _ := auth.GetClaimsFromContext(r.Context())
|
||||||
|
username := ""
|
||||||
|
if claims != nil {
|
||||||
|
username = claims.Username
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(username) == "" {
|
||||||
|
username = "system"
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := mssql.Begin()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
var inserted int64
|
||||||
|
if payload.InsertMissing {
|
||||||
|
inserted, err = queries.InsertMissingVariantsTx(tx, missing, username)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("❌ insert missing error: %v", err)
|
||||||
|
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, err := queries.UpdateOrderLinesTx(tx, id, payload.Lines, username)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("❌ update order lines error: %v", err)
|
||||||
|
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
log.Printf("❌ commit error: %v", err)
|
||||||
|
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := map[string]any{
|
||||||
|
"updated": updated,
|
||||||
|
"inserted": inserted,
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||||
|
log.Printf("❌ encode error: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMissingVariants(mssql *sql.DB, orderHeaderID string, lines []models.OrderProductionUpdateLine) ([]models.OrderProductionMissingVariant, error) {
|
||||||
|
missing := make([]models.OrderProductionMissingVariant, 0)
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
lineID := strings.TrimSpace(line.OrderLineID)
|
||||||
|
newItem := strings.TrimSpace(line.NewItemCode)
|
||||||
|
newColor := strings.TrimSpace(line.NewColor)
|
||||||
|
newDim2 := strings.TrimSpace(line.NewDim2)
|
||||||
|
|
||||||
|
if lineID == "" || newItem == "" || newColor == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
itemTypeCode, dim1, _, dim3, err := queries.GetOrderLineDims(mssql, orderHeaderID, lineID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := queries.VariantExists(mssql, itemTypeCode, newItem, newColor, dim1, newDim2, dim3)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
missing = append(missing, models.OrderProductionMissingVariant{
|
||||||
|
OrderLineID: lineID,
|
||||||
|
ItemTypeCode: itemTypeCode,
|
||||||
|
ItemCode: newItem,
|
||||||
|
ColorCode: newColor,
|
||||||
|
ItemDim1Code: dim1,
|
||||||
|
ItemDim2Code: newDim2,
|
||||||
|
ItemDim3Code: dim3,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return missing, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateUpdateLines(lines []models.OrderProductionUpdateLine) error {
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.TrimSpace(line.OrderLineID) == "" {
|
||||||
|
return errors.New("OrderLineID zorunlu")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(line.NewItemCode) == "" {
|
||||||
|
return errors.New("Yeni urun kodu zorunlu")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(line.NewColor) == "" {
|
||||||
|
return errors.New("Yeni renk kodu zorunlu")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
130
svc/routes/orderproductionlist.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bssapp-backend/auth"
|
||||||
|
"bssapp-backend/db"
|
||||||
|
"bssapp-backend/models"
|
||||||
|
"bssapp-backend/queries"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 📌 OrderProductionListRoute — Üretime verilecek siparişler
|
||||||
|
// ======================================================
|
||||||
|
func OrderProductionListRoute(mssql *sql.DB) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// 🔍 Query Param (RAW + TRIM)
|
||||||
|
// --------------------------------------------------
|
||||||
|
raw := r.URL.Query().Get("search")
|
||||||
|
search := strings.TrimSpace(raw)
|
||||||
|
|
||||||
|
log.Printf(
|
||||||
|
"📥 /api/orders/production-list search raw=%q trimmed=%q lenRaw=%d lenTrim=%d",
|
||||||
|
raw,
|
||||||
|
search,
|
||||||
|
len(raw),
|
||||||
|
len(search),
|
||||||
|
)
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// 🗄 SQL CALL (WITH CONTEXT)
|
||||||
|
// --------------------------------------------------
|
||||||
|
rows, err := queries.GetOrderProductionList(
|
||||||
|
r.Context(),
|
||||||
|
mssql,
|
||||||
|
db.PgDB,
|
||||||
|
search,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("❌ SQL sorgu hatası: %v", err)
|
||||||
|
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// 📦 Sonuç Listesi
|
||||||
|
// --------------------------------------------------
|
||||||
|
list := make([]models.OrderList, 0, 100)
|
||||||
|
count := 0
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// 🧠 SCAN — SQL SELECT ile BİRE BİR (18 kolon)
|
||||||
|
// ==================================================
|
||||||
|
for rows.Next() {
|
||||||
|
|
||||||
|
var o models.OrderList
|
||||||
|
|
||||||
|
err = rows.Scan(
|
||||||
|
&o.OrderHeaderID, // 1
|
||||||
|
&o.OrderNumber, // 2
|
||||||
|
&o.OrderDate, // 3
|
||||||
|
|
||||||
|
&o.CurrAccCode, // 4
|
||||||
|
&o.CurrAccDescription, // 5
|
||||||
|
|
||||||
|
&o.MusteriTemsilcisi, // 6
|
||||||
|
&o.Piyasa, // 7
|
||||||
|
|
||||||
|
&o.CreditableConfirmedDate, // 8
|
||||||
|
&o.DocCurrencyCode, // 9
|
||||||
|
|
||||||
|
&o.TotalAmount, // 10
|
||||||
|
&o.TotalAmountUSD, // 11
|
||||||
|
&o.PackedAmount, // 12
|
||||||
|
&o.PackedUSD, // 13
|
||||||
|
&o.PackedRatePct, // 14
|
||||||
|
|
||||||
|
&o.IsCreditableConfirmed, // 15
|
||||||
|
&o.HasUretimUrunu, // 16
|
||||||
|
&o.Description, // 17
|
||||||
|
|
||||||
|
&o.ExchangeRateUSD, // 18
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(
|
||||||
|
"⚠️ SCAN HATASI | OrderHeaderID=%v | err=%v",
|
||||||
|
o.OrderHeaderID,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
list = append(list, o)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
log.Printf("⚠️ rows.Err(): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// 📊 RESULT LOG
|
||||||
|
// --------------------------------------------------
|
||||||
|
claims, _ := auth.GetClaimsFromContext(r.Context())
|
||||||
|
|
||||||
|
log.Printf(
|
||||||
|
"✅ Order production list DONE | user=%d | search=%q | resultCount=%d",
|
||||||
|
claims.ID,
|
||||||
|
search,
|
||||||
|
count,
|
||||||
|
)
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// ✅ JSON RESPONSE
|
||||||
|
// --------------------------------------------------
|
||||||
|
if err := json.NewEncoder(w).Encode(list); err != nil {
|
||||||
|
log.Printf("❌ encode error: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
230
svc/routes/orderproductionupdate.go
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bssapp-backend/auth"
|
||||||
|
"bssapp-backend/db"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductionUpdateLine struct {
|
||||||
|
OrderLineID string `json:"OrderLineID"`
|
||||||
|
ItemTypeCode int16 `json:"ItemTypeCode"`
|
||||||
|
ItemCode string `json:"ItemCode"`
|
||||||
|
ColorCode string `json:"ColorCode"`
|
||||||
|
ItemDim1Code string `json:"ItemDim1Code"`
|
||||||
|
ItemDim2Code string `json:"ItemDim2Code"`
|
||||||
|
ItemDim3Code string `json:"ItemDim3Code"`
|
||||||
|
LineDescription string `json:"LineDescription"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductionUpdateRequest struct {
|
||||||
|
Lines []ProductionUpdateLine `json:"lines"`
|
||||||
|
InsertMissing bool `json:"insertMissing"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MissingVariant struct {
|
||||||
|
ItemTypeCode int16 `json:"ItemTypeCode"`
|
||||||
|
ItemCode string `json:"ItemCode"`
|
||||||
|
ColorCode string `json:"ColorCode"`
|
||||||
|
ItemDim1Code string `json:"ItemDim1Code"`
|
||||||
|
ItemDim2Code string `json:"ItemDim2Code"`
|
||||||
|
ItemDim3Code string `json:"ItemDim3Code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 📌 OrderProductionUpdateRoute — U ürün satırlarını güncelle
|
||||||
|
// ======================================================
|
||||||
|
func OrderProductionUpdateRoute(mssql *sql.DB) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
|
id := mux.Vars(r)["id"]
|
||||||
|
if id == "" {
|
||||||
|
http.Error(w, "OrderHeaderID bulunamadı", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req ProductionUpdateRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Geçersiz JSON", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Lines) == 0 {
|
||||||
|
http.Error(w, "Satır bulunamadı", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, _ := auth.GetClaimsFromContext(r.Context())
|
||||||
|
username := ""
|
||||||
|
if claims != nil {
|
||||||
|
username = claims.Username
|
||||||
|
}
|
||||||
|
if username == "" {
|
||||||
|
username = "system"
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := db.MssqlDB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "İşlem başlatılamadı", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
// 1) Eksik varyantları kontrol et
|
||||||
|
missingMap := make(map[string]MissingVariant)
|
||||||
|
checkStmt, err := tx.Prepare(`
|
||||||
|
SELECT TOP 1 1
|
||||||
|
FROM dbo.prItemVariant
|
||||||
|
WHERE ItemTypeCode = @p1
|
||||||
|
AND ItemCode = @p2
|
||||||
|
AND ColorCode = @p3
|
||||||
|
AND ISNULL(ItemDim1Code,'') = ISNULL(@p4,'')
|
||||||
|
AND ISNULL(ItemDim2Code,'') = ISNULL(@p5,'')
|
||||||
|
AND ISNULL(ItemDim3Code,'') = ISNULL(@p6,'')
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Varyant kontrolü hazırlanamadı", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer checkStmt.Close()
|
||||||
|
|
||||||
|
for _, ln := range req.Lines {
|
||||||
|
if strings.TrimSpace(ln.ItemCode) == "" {
|
||||||
|
http.Error(w, "Yeni model kodu boş", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
row := checkStmt.QueryRow(
|
||||||
|
ln.ItemTypeCode,
|
||||||
|
ln.ItemCode,
|
||||||
|
ln.ColorCode,
|
||||||
|
ln.ItemDim1Code,
|
||||||
|
ln.ItemDim2Code,
|
||||||
|
ln.ItemDim3Code,
|
||||||
|
)
|
||||||
|
var ok int
|
||||||
|
if err := row.Scan(&ok); err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
key := strings.Join([]string{
|
||||||
|
ln.ItemCode, ln.ColorCode, ln.ItemDim1Code, ln.ItemDim2Code, ln.ItemDim3Code,
|
||||||
|
}, "|")
|
||||||
|
missingMap[key] = MissingVariant{
|
||||||
|
ItemTypeCode: ln.ItemTypeCode,
|
||||||
|
ItemCode: ln.ItemCode,
|
||||||
|
ColorCode: ln.ColorCode,
|
||||||
|
ItemDim1Code: ln.ItemDim1Code,
|
||||||
|
ItemDim2Code: ln.ItemDim2Code,
|
||||||
|
ItemDim3Code: ln.ItemDim3Code,
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
http.Error(w, "Varyant kontrolü hatası", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missingMap) > 0 && !req.InsertMissing {
|
||||||
|
missing := make([]MissingVariant, 0, len(missingMap))
|
||||||
|
for _, v := range missingMap {
|
||||||
|
missing = append(missing, v)
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusConflict)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"missing": missing,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Eksikleri ekle (gerekirse)
|
||||||
|
if len(missingMap) > 0 {
|
||||||
|
// PLU üretimi (max + row_number)
|
||||||
|
var basePlu int64
|
||||||
|
if err := tx.QueryRow(`SELECT ISNULL(MAX(PLU),0) FROM dbo.prItemVariant WITH (UPDLOCK, HOLDLOCK)`).Scan(&basePlu); err != nil {
|
||||||
|
http.Error(w, "PLU alınamadı", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
i := int64(0)
|
||||||
|
for _, v := range missingMap {
|
||||||
|
i++
|
||||||
|
if _, err := tx.Exec(`
|
||||||
|
INSERT INTO dbo.prItemVariant
|
||||||
|
(
|
||||||
|
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code,
|
||||||
|
PLU, CreatedUserName, CreatedDate, LastUpdatedUserName, LastUpdatedDate
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(@p1,@p2,@p3,@p4,@p5,@p6,@p7,@p8,@p9,@p8,@p9)
|
||||||
|
`,
|
||||||
|
v.ItemTypeCode,
|
||||||
|
v.ItemCode,
|
||||||
|
v.ColorCode,
|
||||||
|
v.ItemDim1Code,
|
||||||
|
v.ItemDim2Code,
|
||||||
|
v.ItemDim3Code,
|
||||||
|
basePlu+i,
|
||||||
|
username,
|
||||||
|
now,
|
||||||
|
); err != nil {
|
||||||
|
http.Error(w, "Varyant insert hatası", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) trOrderLine güncelle
|
||||||
|
updStmt, err := tx.Prepare(`
|
||||||
|
UPDATE dbo.trOrderLine
|
||||||
|
SET
|
||||||
|
ItemCode = @p1,
|
||||||
|
ColorCode = @p2,
|
||||||
|
ItemDim2Code = @p3,
|
||||||
|
LineDescription = @p4,
|
||||||
|
LastUpdatedUserName = @p5,
|
||||||
|
LastUpdatedDate = @p6
|
||||||
|
WHERE OrderHeaderID = @p7
|
||||||
|
AND OrderLineID = @p8
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Update hazırlığı başarısız", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer updStmt.Close()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
for _, ln := range req.Lines {
|
||||||
|
if _, err := updStmt.Exec(
|
||||||
|
ln.ItemCode,
|
||||||
|
ln.ColorCode,
|
||||||
|
ln.ItemDim2Code,
|
||||||
|
ln.LineDescription,
|
||||||
|
username,
|
||||||
|
now,
|
||||||
|
id,
|
||||||
|
ln.OrderLineID,
|
||||||
|
); err != nil {
|
||||||
|
http.Error(w, "Satır güncelleme hatası", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
http.Error(w, "Commit hatası", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"status": "ok",
|
||||||
|
"updated": len(req.Lines),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -6,9 +6,7 @@ import (
|
|||||||
"bssapp-backend/internal/security"
|
"bssapp-backend/internal/security"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -75,7 +73,7 @@ func ForgotPasswordHandler(
|
|||||||
_, _ = db.Exec(`
|
_, _ = db.Exec(`
|
||||||
INSERT INTO dfusr_password_reset (
|
INSERT INTO dfusr_password_reset (
|
||||||
dfusr_id,
|
dfusr_id,
|
||||||
token,
|
token_hash,
|
||||||
expires_at
|
expires_at
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3)
|
VALUES ($1, $2, $3)
|
||||||
@@ -84,11 +82,7 @@ func ForgotPasswordHandler(
|
|||||||
// -------------------------------------------------------
|
// -------------------------------------------------------
|
||||||
// 5️⃣ Reset URL (PLAIN token)
|
// 5️⃣ Reset URL (PLAIN token)
|
||||||
// -------------------------------------------------------
|
// -------------------------------------------------------
|
||||||
resetURL := fmt.Sprintf(
|
resetURL := security.BuildResetURL(plain)
|
||||||
"%s/password-reset/%s",
|
|
||||||
os.Getenv("FRONTEND_URL"),
|
|
||||||
plain,
|
|
||||||
)
|
|
||||||
|
|
||||||
// -------------------------------------------------------
|
// -------------------------------------------------------
|
||||||
// 6️⃣ Mail gönder (fail olsa bile enumeration yok)
|
// 6️⃣ Mail gönder (fail olsa bile enumeration yok)
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ func CompletePasswordResetHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
var expiresAt time.Time
|
var expiresAt time.Time
|
||||||
|
|
||||||
err := db.QueryRow(`
|
err := db.QueryRow(`
|
||||||
SELECT mk_dfusr_id, expires_at
|
SELECT dfusr_id, expires_at
|
||||||
FROM mk_dfusr_password_reset
|
FROM dfusr_password_reset
|
||||||
WHERE token = $1
|
WHERE token_hash = $1
|
||||||
AND used_at IS NULL
|
AND used_at IS NULL
|
||||||
`, tokenHash).Scan(&userID, &expiresAt)
|
`, tokenHash).Scan(&userID, &expiresAt)
|
||||||
|
|
||||||
@@ -84,9 +84,10 @@ func CompletePasswordResetHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
|
|
||||||
// token tüket
|
// token tüket
|
||||||
if _, err := tx.Exec(`
|
if _, err := tx.Exec(`
|
||||||
UPDATE mk_dfusr_password_reset
|
UPDATE dfusr_password_reset
|
||||||
SET used_at = now()
|
SET used_at = now()
|
||||||
WHERE token = $1
|
WHERE token_hash = $1
|
||||||
|
AND used_at IS NULL
|
||||||
`, tokenHash); err != nil {
|
`, tokenHash); err != nil {
|
||||||
http.Error(w, "token update failed", http.StatusInternalServerError)
|
http.Error(w, "token update failed", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"bssapp-backend/internal/security"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/hex"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -21,8 +20,7 @@ func ValidatePasswordResetTokenHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🔐 plain token -> hash
|
// 🔐 plain token -> hash
|
||||||
h := sha256.Sum256([]byte(token))
|
tokenHash := security.HashToken(token)
|
||||||
tokenHash := hex.EncodeToString(h[:])
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
userID int64
|
userID int64
|
||||||
@@ -31,8 +29,8 @@ func ValidatePasswordResetTokenHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
)
|
)
|
||||||
|
|
||||||
err := db.QueryRow(`
|
err := db.QueryRow(`
|
||||||
SELECT user_id, expires_at, used_at
|
SELECT dfusr_id, expires_at, used_at
|
||||||
FROM password_reset_tokens
|
FROM dfusr_password_reset
|
||||||
WHERE token_hash = $1
|
WHERE token_hash = $1
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`, tokenHash).Scan(&userID, &expiresAt, &usedAt)
|
`, tokenHash).Scan(&userID, &expiresAt, &usedAt)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
type IdTitleOption struct {
|
type IdTitleOption struct {
|
||||||
@@ -57,7 +58,7 @@ type RoleDepartmentPermissionHandler struct {
|
|||||||
func NewRoleDepartmentPermissionHandler(db *sql.DB) *RoleDepartmentPermissionHandler {
|
func NewRoleDepartmentPermissionHandler(db *sql.DB) *RoleDepartmentPermissionHandler {
|
||||||
|
|
||||||
return &RoleDepartmentPermissionHandler{
|
return &RoleDepartmentPermissionHandler{
|
||||||
DB: db, // ✅ EKLENDİ
|
DB: db, // Added
|
||||||
Repo: permissions.NewRoleDepartmentPermissionRepo(db),
|
Repo: permissions.NewRoleDepartmentPermissionRepo(db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,7 +418,7 @@ func (h *PermissionHandler) GetUserOverrides(w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
list, err := h.Repo.GetUserOverridesByUserID(userID)
|
list, err := h.Repo.GetUserOverridesByUserID(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("❌ USER OVERRIDE LOAD ERROR:", err)
|
log.Println("USER OVERRIDE LOAD ERROR:", err)
|
||||||
http.Error(w, "db error", http.StatusInternalServerError)
|
http.Error(w, "db error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -425,6 +426,138 @@ func (h *PermissionHandler) GetUserOverrides(w http.ResponseWriter, r *http.Requ
|
|||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
_ = json.NewEncoder(w).Encode(list)
|
_ = json.NewEncoder(w).Encode(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type routePermissionSeed struct {
|
||||||
|
Module string
|
||||||
|
Action string
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
type moduleActionSeed struct {
|
||||||
|
Module string
|
||||||
|
Action string
|
||||||
|
}
|
||||||
|
|
||||||
|
type permissionSnapshot struct {
|
||||||
|
user map[string]bool
|
||||||
|
roleDept map[string]bool
|
||||||
|
role map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func permissionKey(module, action string) string {
|
||||||
|
return module + "|" + action
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadPermissionSnapshot(
|
||||||
|
db *sql.DB,
|
||||||
|
userID int64,
|
||||||
|
roleID int64,
|
||||||
|
deptCodes []string,
|
||||||
|
) (permissionSnapshot, error) {
|
||||||
|
snapshot := permissionSnapshot{
|
||||||
|
user: make(map[string]bool, 128),
|
||||||
|
roleDept: make(map[string]bool, 128),
|
||||||
|
role: make(map[string]bool, 128),
|
||||||
|
}
|
||||||
|
|
||||||
|
userRows, err := db.Query(`
|
||||||
|
SELECT module_code, action, allowed
|
||||||
|
FROM mk_sys_user_permissions
|
||||||
|
WHERE user_id = $1
|
||||||
|
`, userID)
|
||||||
|
if err != nil {
|
||||||
|
return snapshot, err
|
||||||
|
}
|
||||||
|
for userRows.Next() {
|
||||||
|
var module, action string
|
||||||
|
var allowed bool
|
||||||
|
if err := userRows.Scan(&module, &action, &allowed); err != nil {
|
||||||
|
_ = userRows.Close()
|
||||||
|
return snapshot, err
|
||||||
|
}
|
||||||
|
snapshot.user[permissionKey(module, action)] = allowed
|
||||||
|
}
|
||||||
|
if err := userRows.Err(); err != nil {
|
||||||
|
_ = userRows.Close()
|
||||||
|
return snapshot, err
|
||||||
|
}
|
||||||
|
_ = userRows.Close()
|
||||||
|
|
||||||
|
if len(deptCodes) > 0 {
|
||||||
|
roleDeptRows, err := db.Query(`
|
||||||
|
SELECT module_code, action, BOOL_OR(allowed) AS allowed
|
||||||
|
FROM vw_role_dept_permissions
|
||||||
|
WHERE role_id = $1
|
||||||
|
AND department_code = ANY($2)
|
||||||
|
GROUP BY module_code, action
|
||||||
|
`,
|
||||||
|
roleID,
|
||||||
|
pq.Array(deptCodes),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return snapshot, err
|
||||||
|
}
|
||||||
|
for roleDeptRows.Next() {
|
||||||
|
var module, action string
|
||||||
|
var allowed bool
|
||||||
|
if err := roleDeptRows.Scan(&module, &action, &allowed); err != nil {
|
||||||
|
_ = roleDeptRows.Close()
|
||||||
|
return snapshot, err
|
||||||
|
}
|
||||||
|
snapshot.roleDept[permissionKey(module, action)] = allowed
|
||||||
|
}
|
||||||
|
if err := roleDeptRows.Err(); err != nil {
|
||||||
|
_ = roleDeptRows.Close()
|
||||||
|
return snapshot, err
|
||||||
|
}
|
||||||
|
_ = roleDeptRows.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
roleRows, err := db.Query(`
|
||||||
|
SELECT module_code, action, allowed
|
||||||
|
FROM mk_sys_role_permissions
|
||||||
|
WHERE role_id = $1
|
||||||
|
`, roleID)
|
||||||
|
if err != nil {
|
||||||
|
return snapshot, err
|
||||||
|
}
|
||||||
|
for roleRows.Next() {
|
||||||
|
var module, action string
|
||||||
|
var allowed bool
|
||||||
|
if err := roleRows.Scan(&module, &action, &allowed); err != nil {
|
||||||
|
_ = roleRows.Close()
|
||||||
|
return snapshot, err
|
||||||
|
}
|
||||||
|
snapshot.role[permissionKey(module, action)] = allowed
|
||||||
|
}
|
||||||
|
if err := roleRows.Err(); err != nil {
|
||||||
|
_ = roleRows.Close()
|
||||||
|
return snapshot, err
|
||||||
|
}
|
||||||
|
_ = roleRows.Close()
|
||||||
|
|
||||||
|
return snapshot, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolvePermissionFromSnapshot(
|
||||||
|
s permissionSnapshot,
|
||||||
|
module string,
|
||||||
|
action string,
|
||||||
|
) bool {
|
||||||
|
key := permissionKey(module, action)
|
||||||
|
|
||||||
|
if allowed, ok := s.user[key]; ok {
|
||||||
|
return allowed
|
||||||
|
}
|
||||||
|
if allowed, ok := s.roleDept[key]; ok {
|
||||||
|
return allowed
|
||||||
|
}
|
||||||
|
if allowed, ok := s.role[key]; ok {
|
||||||
|
return allowed
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func GetUserRoutePermissionsHandler(db *sql.DB) http.HandlerFunc {
|
func GetUserRoutePermissionsHandler(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
@@ -436,10 +569,16 @@ func GetUserRoutePermissionsHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
repo := permissions.NewPermissionRepository(db)
|
snapshot, err := loadPermissionSnapshot(
|
||||||
|
db,
|
||||||
// JWT’den departmanlar
|
int64(claims.ID),
|
||||||
depts := claims.DepartmentCodes
|
int64(claims.RoleID),
|
||||||
|
claims.DepartmentCodes,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
rows, err := db.Query(`
|
rows, err := db.Query(`
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
@@ -454,17 +593,9 @@ func GetUserRoutePermissionsHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
type Row struct {
|
routeSeeds := make([]routePermissionSeed, 0, 128)
|
||||||
Route string `json:"route"`
|
|
||||||
CanAccess bool `json:"can_access"`
|
|
||||||
}
|
|
||||||
|
|
||||||
list := make([]Row, 0, 64)
|
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
|
|
||||||
var module, action, path string
|
var module, action, path string
|
||||||
|
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&module,
|
&module,
|
||||||
&action,
|
&action,
|
||||||
@@ -473,22 +604,26 @@ func GetUserRoutePermissionsHandler(db *sql.DB) http.HandlerFunc {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
allowed, err := repo.ResolvePermissionChain(
|
routeSeeds = append(routeSeeds, routePermissionSeed{
|
||||||
int64(claims.ID),
|
Module: module,
|
||||||
int64(claims.RoleID),
|
Action: action,
|
||||||
depts,
|
Path: path,
|
||||||
module,
|
})
|
||||||
action,
|
}
|
||||||
)
|
if err := rows.Err(); err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
if err != nil {
|
return
|
||||||
log.Println("PERM RESOLVE ERROR:", err)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
list := make([]Row, 0, len(routeSeeds))
|
||||||
|
for _, route := range routeSeeds {
|
||||||
list = append(list, Row{
|
list = append(list, Row{
|
||||||
Route: path,
|
Route: route.Path,
|
||||||
CanAccess: allowed,
|
CanAccess: resolvePermissionFromSnapshot(
|
||||||
|
snapshot,
|
||||||
|
route.Module,
|
||||||
|
route.Action,
|
||||||
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,18 +642,17 @@ func GetMyEffectivePermissions(db *sql.DB) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
repo := permissions.NewPermissionRepository(db)
|
snapshot, err := loadPermissionSnapshot(
|
||||||
|
db,
|
||||||
// ✅ JWT'DEN DEPARTMENTS
|
int64(claims.ID),
|
||||||
depts := claims.DepartmentCodes
|
int64(claims.RoleID),
|
||||||
|
claims.DepartmentCodes,
|
||||||
log.Printf("🧪 EFFECTIVE PERM | user=%d role=%d depts=%v",
|
|
||||||
claims.ID,
|
|
||||||
claims.RoleID,
|
|
||||||
depts,
|
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// all system perms
|
|
||||||
all, err := db.Query(`
|
all, err := db.Query(`
|
||||||
SELECT DISTINCT module_code, action
|
SELECT DISTINCT module_code, action
|
||||||
FROM mk_sys_routes
|
FROM mk_sys_routes
|
||||||
@@ -529,14 +663,7 @@ func GetMyEffectivePermissions(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
defer all.Close()
|
defer all.Close()
|
||||||
|
|
||||||
type Row struct {
|
moduleActions := make([]moduleActionSeed, 0, 128)
|
||||||
Module string `json:"module"`
|
|
||||||
Action string `json:"action"`
|
|
||||||
Allowed bool `json:"allowed"`
|
|
||||||
}
|
|
||||||
|
|
||||||
list := make([]Row, 0, 128)
|
|
||||||
|
|
||||||
for all.Next() {
|
for all.Next() {
|
||||||
|
|
||||||
var m, a string
|
var m, a string
|
||||||
@@ -544,22 +671,32 @@ func GetMyEffectivePermissions(db *sql.DB) http.HandlerFunc {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
allowed, err := repo.ResolvePermissionChain(
|
moduleActions = append(moduleActions, moduleActionSeed{
|
||||||
int64(claims.ID),
|
|
||||||
int64(claims.RoleID),
|
|
||||||
depts,
|
|
||||||
m,
|
|
||||||
a,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
list = append(list, Row{
|
|
||||||
Module: m,
|
Module: m,
|
||||||
Action: a,
|
Action: a,
|
||||||
Allowed: allowed,
|
})
|
||||||
|
}
|
||||||
|
if err := all.Err(); err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type Row struct {
|
||||||
|
Module string `json:"module"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
Allowed bool `json:"allowed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
list := make([]Row, 0, len(moduleActions))
|
||||||
|
for _, item := range moduleActions {
|
||||||
|
list = append(list, Row{
|
||||||
|
Module: item.Module,
|
||||||
|
Action: item.Action,
|
||||||
|
Allowed: resolvePermissionFromSnapshot(
|
||||||
|
snapshot,
|
||||||
|
item.Module,
|
||||||
|
item.Action,
|
||||||
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ======================================================
|
// ======================================================
|
||||||
@@ -374,12 +374,42 @@ func handleUserDelete(db *sql.DB, w http.ResponseWriter, r *http.Request, userID
|
|||||||
`DELETE FROM dfusr_nebim_user WHERE dfusr_id = $1`,
|
`DELETE FROM dfusr_nebim_user WHERE dfusr_id = $1`,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, q := range cleanupQueries {
|
isUndefinedTable := func(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if pe, ok := err.(*pq.Error); ok {
|
||||||
|
return pe.Code == "42P01" // undefined_table
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, q := range cleanupQueries {
|
||||||
|
sp := fmt.Sprintf("sp_user_cleanup_%d", i)
|
||||||
|
_, _ = tx.Exec("SAVEPOINT " + sp)
|
||||||
|
|
||||||
if _, err := tx.Exec(q, userID); err != nil {
|
if _, err := tx.Exec(q, userID); err != nil {
|
||||||
|
// rollback the failed statement so tx can continue
|
||||||
|
_, _ = tx.Exec("ROLLBACK TO SAVEPOINT " + sp)
|
||||||
|
|
||||||
|
if isUndefinedTable(err) {
|
||||||
|
log.Printf("⚠️ [UserDetail] cleanup skipped (table missing) user_id=%d query=%s", userID, q)
|
||||||
|
_, _ = tx.Exec("RELEASE SAVEPOINT " + sp)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if pe, ok := err.(*pq.Error); ok {
|
||||||
|
log.Printf(
|
||||||
|
"❌ [UserDetail] cleanup failed user_id=%d code=%s detail=%s constraint=%s table=%s err=%v query=%s",
|
||||||
|
userID, pe.Code, pe.Detail, pe.Constraint, pe.Table, err, q,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
log.Printf("❌ [UserDetail] cleanup failed user_id=%d err=%v query=%s", userID, err, q)
|
log.Printf("❌ [UserDetail] cleanup failed user_id=%d err=%v query=%s", userID, err, q)
|
||||||
|
}
|
||||||
http.Error(w, "Kullanici baglantilari silinemedi", http.StatusInternalServerError)
|
http.Error(w, "Kullanici baglantilari silinemedi", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, _ = tx.Exec("RELEASE SAVEPOINT " + sp)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := tx.Exec(`DELETE FROM mk_dfusr WHERE id = $1`, userID); err != nil {
|
if _, err := tx.Exec(`DELETE FROM mk_dfusr WHERE id = $1`, userID); err != nil {
|
||||||
@@ -388,12 +418,6 @@ func handleUserDelete(db *sql.DB, w http.ResponseWriter, r *http.Request, userID
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := tx.Exec(`DELETE FROM dfusr WHERE id = $1`, userID); err != nil {
|
|
||||||
log.Printf("❌ [UserDetail] delete dfusr failed user_id=%d err=%v", userID, err)
|
|
||||||
http.Error(w, "Kullanici silinemedi", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
log.Printf("❌ [UserDetail] delete commit failed user_id=%d err=%v", userID, err)
|
log.Printf("❌ [UserDetail] delete commit failed user_id=%d err=%v", userID, err)
|
||||||
http.Error(w, "Commit basarisiz", http.StatusInternalServerError)
|
http.Error(w, "Commit basarisiz", http.StatusInternalServerError)
|
||||||
@@ -405,7 +429,7 @@ func handleUserDelete(db *sql.DB, w http.ResponseWriter, r *http.Request, userID
|
|||||||
ActionType: "user_delete",
|
ActionType: "user_delete",
|
||||||
ActionCategory: "user_admin",
|
ActionCategory: "user_admin",
|
||||||
ActionTarget: fmt.Sprintf("/api/users/%d", userID),
|
ActionTarget: fmt.Sprintf("/api/users/%d", userID),
|
||||||
Description: "user deleted from mk_dfusr and dfusr",
|
Description: "user deleted from mk_dfusr (dfusr retained)",
|
||||||
Username: claims.Username,
|
Username: claims.Username,
|
||||||
RoleCode: claims.RoleCode,
|
RoleCode: claims.RoleCode,
|
||||||
DfUsrID: int64(claims.ID),
|
DfUsrID: int64(claims.ID),
|
||||||
@@ -457,16 +481,12 @@ func SendPasswordResetMailHandler(
|
|||||||
|
|
||||||
// 💾 DB → SADECE HASH
|
// 💾 DB → SADECE HASH
|
||||||
_, _ = db.Exec(`
|
_, _ = db.Exec(`
|
||||||
INSERT INTO dfusr_password_reset (dfusr_id, token, expires_at)
|
INSERT INTO dfusr_password_reset (dfusr_id, token_hash, expires_at)
|
||||||
VALUES ($1,$2,$3)
|
VALUES ($1,$2,$3)
|
||||||
`, userID, hash, expires)
|
`, userID, hash, expires)
|
||||||
|
|
||||||
// 🔗 URL → PLAIN
|
// 🔗 URL → PLAIN
|
||||||
resetURL := fmt.Sprintf(
|
resetURL := security.BuildResetURL(plain)
|
||||||
"%s/password-reset/%s",
|
|
||||||
os.Getenv("FRONTEND_URL"),
|
|
||||||
plain,
|
|
||||||
)
|
|
||||||
|
|
||||||
_ = mailer.SendPasswordResetMail(email, resetURL)
|
_ = mailer.SendPasswordResetMail(email, resetURL)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bssapp-backend/models"
|
"bssapp-backend/models"
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
@@ -16,7 +20,6 @@ func CheckPasswordWithLegacy(user *models.User, plain string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
plain = strings.TrimSpace(plain)
|
|
||||||
if plain == "" {
|
if plain == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -28,11 +31,100 @@ func CheckPasswordWithLegacy(user *models.User, plain string) bool {
|
|||||||
|
|
||||||
// 1️⃣ bcrypt hash mi?
|
// 1️⃣ bcrypt hash mi?
|
||||||
if isBcryptHash(stored) {
|
if isBcryptHash(stored) {
|
||||||
return bcrypt.CompareHashAndPassword([]byte(stored), []byte(plain)) == nil
|
candidates := make([]string, 0, 10)
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
add := func(v string) {
|
||||||
|
if v == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := seen[v]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen[v] = struct{}{}
|
||||||
|
candidates = append(candidates, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
add(plain)
|
||||||
|
trimmed := strings.TrimSpace(plain)
|
||||||
|
add(trimmed)
|
||||||
|
|
||||||
|
bases := append([]string(nil), candidates...)
|
||||||
|
for _, base := range bases {
|
||||||
|
md5Sum := md5.Sum([]byte(base))
|
||||||
|
md5Hex := hex.EncodeToString(md5Sum[:])
|
||||||
|
add(md5Hex)
|
||||||
|
add(strings.ToUpper(md5Hex))
|
||||||
|
|
||||||
|
sha1Sum := sha1.Sum([]byte(base))
|
||||||
|
sha1Hex := hex.EncodeToString(sha1Sum[:])
|
||||||
|
add(sha1Hex)
|
||||||
|
add(strings.ToUpper(sha1Hex))
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(stored), []byte(candidate)); err == nil {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
lastErr = err
|
||||||
|
}
|
||||||
|
if encoded, ok := encodeLegacySingleByte(candidate); ok {
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(stored), encoded); err == nil {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
lastErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lastErr != nil {
|
||||||
|
log.Printf(
|
||||||
|
"LEGACY BCRYPT MISMATCH stored_len=%d candidates=%d last_err=%v",
|
||||||
|
len(stored),
|
||||||
|
len(candidates),
|
||||||
|
lastErr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2️⃣ TAM LEGACY — düz metin (eski kayıtlar)
|
// 2️⃣ TAM LEGACY — düz metin (eski kayıtlar)
|
||||||
return stored == plain
|
if stored == plain {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
trimmed := strings.TrimSpace(plain)
|
||||||
|
if trimmed != plain && trimmed != "" && stored == trimmed {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3️⃣ Legacy hash variants seen in old dfusr.upass data.
|
||||||
|
if isHexDigest(stored, 32) {
|
||||||
|
sumRaw := md5.Sum([]byte(plain))
|
||||||
|
if strings.EqualFold(stored, hex.EncodeToString(sumRaw[:])) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if trimmed != plain && trimmed != "" {
|
||||||
|
sumTrim := md5.Sum([]byte(trimmed))
|
||||||
|
if strings.EqualFold(stored, hex.EncodeToString(sumTrim[:])) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isHexDigest(stored, 40) {
|
||||||
|
sumRaw := sha1.Sum([]byte(plain))
|
||||||
|
if strings.EqualFold(stored, hex.EncodeToString(sumRaw[:])) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if trimmed != plain && trimmed != "" {
|
||||||
|
sumTrim := sha1.Sum([]byte(trimmed))
|
||||||
|
if strings.EqualFold(stored, hex.EncodeToString(sumTrim[:])) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func isBcryptHash(s string) bool {
|
func isBcryptHash(s string) bool {
|
||||||
@@ -40,3 +132,46 @@ func isBcryptHash(s string) bool {
|
|||||||
strings.HasPrefix(s, "$2b$") ||
|
strings.HasPrefix(s, "$2b$") ||
|
||||||
strings.HasPrefix(s, "$2y$")
|
strings.HasPrefix(s, "$2y$")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isHexDigest(s string, expectedLen int) bool {
|
||||||
|
if len(s) != expectedLen {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, r := range s {
|
||||||
|
if (r < '0' || r > '9') &&
|
||||||
|
(r < 'a' || r > 'f') &&
|
||||||
|
(r < 'A' || r > 'F') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeLegacySingleByte converts text to a Turkish-compatible single-byte
|
||||||
|
// representation (similar to Windows-1254 / ISO-8859-9) for legacy bcrypt data.
|
||||||
|
func encodeLegacySingleByte(s string) ([]byte, bool) {
|
||||||
|
out := make([]byte, 0, len(s))
|
||||||
|
for _, r := range s {
|
||||||
|
switch r {
|
||||||
|
case 'Ğ':
|
||||||
|
out = append(out, 0xD0)
|
||||||
|
case 'ğ':
|
||||||
|
out = append(out, 0xF0)
|
||||||
|
case 'İ':
|
||||||
|
out = append(out, 0xDD)
|
||||||
|
case 'ı':
|
||||||
|
out = append(out, 0xFD)
|
||||||
|
case 'Ş':
|
||||||
|
out = append(out, 0xDE)
|
||||||
|
case 'ş':
|
||||||
|
out = append(out, 0xFE)
|
||||||
|
default:
|
||||||
|
if r >= 0 && r <= 0xFF {
|
||||||
|
out = append(out, byte(r))
|
||||||
|
} else {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, true
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
VITE_API_BASE_URL=http://localhost:8080
|
VITE_API_BASE_URL=http://localhost:8080/api
|
||||||
|
|||||||
@@ -146,8 +146,6 @@ createQuasarApp(createApp, quasarUserOptions)
|
|||||||
|
|
||||||
return Promise[ method ]([
|
return Promise[ method ]([
|
||||||
|
|
||||||
import(/* webpackMode: "eager" */ 'boot/axios'),
|
|
||||||
|
|
||||||
import(/* webpackMode: "eager" */ 'boot/dayjs')
|
import(/* webpackMode: "eager" */ 'boot/dayjs')
|
||||||
|
|
||||||
]).then(bootFiles => {
|
]).then(bootFiles => {
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/**
|
|
||||||
* THIS FILE IS GENERATED AUTOMATICALLY.
|
|
||||||
* DO NOT EDIT.
|
|
||||||
*
|
|
||||||
* You are probably looking on adding startup/initialization code.
|
|
||||||
* Use "quasar new boot <name>" and add it there.
|
|
||||||
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
|
|
||||||
* boot: ['file', ...] // do not add ".js" extension to it.
|
|
||||||
*
|
|
||||||
* Boot files are your "main.js"
|
|
||||||
**/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import { Quasar } from 'quasar'
|
|
||||||
import { markRaw } from 'vue'
|
|
||||||
import RootComponent from 'app/src/App.vue'
|
|
||||||
|
|
||||||
import createStore from 'app/src/stores/index'
|
|
||||||
import createRouter from 'app/src/router/index'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default async function (createAppFn, quasarUserOptions) {
|
|
||||||
|
|
||||||
|
|
||||||
// Create the app instance.
|
|
||||||
// Here we inject into it the Quasar UI, the router & possibly the store.
|
|
||||||
const app = createAppFn(RootComponent)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.use(Quasar, quasarUserOptions)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const store = typeof createStore === 'function'
|
|
||||||
? await createStore({})
|
|
||||||
: createStore
|
|
||||||
|
|
||||||
|
|
||||||
app.use(store)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const router = markRaw(
|
|
||||||
typeof createRouter === 'function'
|
|
||||||
? await createRouter({store})
|
|
||||||
: createRouter
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
// make router instance available in store
|
|
||||||
|
|
||||||
store.use(({ store }) => { store.router = router })
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Expose the app, the router and the store.
|
|
||||||
// Note that we are not mounting the app here, since bootstrapping will be
|
|
||||||
// different depending on whether we are in a browser or on the server.
|
|
||||||
return {
|
|
||||||
app,
|
|
||||||
store,
|
|
||||||
router
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/**
|
|
||||||
* THIS FILE IS GENERATED AUTOMATICALLY.
|
|
||||||
* DO NOT EDIT.
|
|
||||||
*
|
|
||||||
* You are probably looking on adding startup/initialization code.
|
|
||||||
* Use "quasar new boot <name>" and add it there.
|
|
||||||
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
|
|
||||||
* boot: ['file', ...] // do not add ".js" extension to it.
|
|
||||||
*
|
|
||||||
* Boot files are your "main.js"
|
|
||||||
**/
|
|
||||||
|
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import '@quasar/extras/roboto-font/roboto-font.css'
|
|
||||||
|
|
||||||
import '@quasar/extras/material-icons/material-icons.css'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// We load Quasar stylesheet file
|
|
||||||
import 'quasar/dist/quasar.sass'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import 'src/css/app.css'
|
|
||||||
|
|
||||||
|
|
||||||
import createQuasarApp from './app.js'
|
|
||||||
import quasarUserOptions from './quasar-user-options.js'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const publicPath = `/`
|
|
||||||
|
|
||||||
|
|
||||||
async function start ({
|
|
||||||
app,
|
|
||||||
router
|
|
||||||
, store
|
|
||||||
}, bootFiles) {
|
|
||||||
|
|
||||||
let hasRedirected = false
|
|
||||||
const getRedirectUrl = url => {
|
|
||||||
try { return router.resolve(url).href }
|
|
||||||
catch (err) {}
|
|
||||||
|
|
||||||
return Object(url) === url
|
|
||||||
? null
|
|
||||||
: url
|
|
||||||
}
|
|
||||||
const redirect = url => {
|
|
||||||
hasRedirected = true
|
|
||||||
|
|
||||||
if (typeof url === 'string' && /^https?:\/\//.test(url)) {
|
|
||||||
window.location.href = url
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const href = getRedirectUrl(url)
|
|
||||||
|
|
||||||
// continue if we didn't fail to resolve the url
|
|
||||||
if (href !== null) {
|
|
||||||
window.location.href = href
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const urlPath = window.location.href.replace(window.location.origin, '')
|
|
||||||
|
|
||||||
for (let i = 0; hasRedirected === false && i < bootFiles.length; i++) {
|
|
||||||
try {
|
|
||||||
await bootFiles[i]({
|
|
||||||
app,
|
|
||||||
router,
|
|
||||||
store,
|
|
||||||
ssrContext: null,
|
|
||||||
redirect,
|
|
||||||
urlPath,
|
|
||||||
publicPath
|
|
||||||
})
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
if (err && err.url) {
|
|
||||||
redirect(err.url)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('[Quasar] boot error:', err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasRedirected === true) return
|
|
||||||
|
|
||||||
|
|
||||||
app.use(router)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.mount('#q-app')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
createQuasarApp(createApp, quasarUserOptions)
|
|
||||||
|
|
||||||
.then(app => {
|
|
||||||
// eventually remove this when Cordova/Capacitor/Electron support becomes old
|
|
||||||
const [ method, mapFn ] = Promise.allSettled !== void 0
|
|
||||||
? [
|
|
||||||
'allSettled',
|
|
||||||
bootFiles => bootFiles.map(result => {
|
|
||||||
if (result.status === 'rejected') {
|
|
||||||
console.error('[Quasar] boot error:', result.reason)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return result.value.default
|
|
||||||
})
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
'all',
|
|
||||||
bootFiles => bootFiles.map(entry => entry.default)
|
|
||||||
]
|
|
||||||
|
|
||||||
return Promise[ method ]([
|
|
||||||
|
|
||||||
import(/* webpackMode: "eager" */ 'boot/dayjs')
|
|
||||||
|
|
||||||
]).then(bootFiles => {
|
|
||||||
const boot = mapFn(bootFiles).filter(entry => typeof entry === 'function')
|
|
||||||
start(app, boot)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/**
|
|
||||||
* THIS FILE IS GENERATED AUTOMATICALLY.
|
|
||||||
* DO NOT EDIT.
|
|
||||||
*
|
|
||||||
* You are probably looking on adding startup/initialization code.
|
|
||||||
* Use "quasar new boot <name>" and add it there.
|
|
||||||
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
|
|
||||||
* boot: ['file', ...] // do not add ".js" extension to it.
|
|
||||||
*
|
|
||||||
* Boot files are your "main.js"
|
|
||||||
**/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import App from 'app/src/App.vue'
|
|
||||||
let appPrefetch = typeof App.preFetch === 'function'
|
|
||||||
? App.preFetch
|
|
||||||
: (
|
|
||||||
// Class components return the component options (and the preFetch hook) inside __c property
|
|
||||||
App.__c !== void 0 && typeof App.__c.preFetch === 'function'
|
|
||||||
? App.__c.preFetch
|
|
||||||
: false
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
function getMatchedComponents (to, router) {
|
|
||||||
const route = to
|
|
||||||
? (to.matched ? to : router.resolve(to).route)
|
|
||||||
: router.currentRoute.value
|
|
||||||
|
|
||||||
if (!route) { return [] }
|
|
||||||
|
|
||||||
const matched = route.matched.filter(m => m.components !== void 0)
|
|
||||||
|
|
||||||
if (matched.length === 0) { return [] }
|
|
||||||
|
|
||||||
return Array.prototype.concat.apply([], matched.map(m => {
|
|
||||||
return Object.keys(m.components).map(key => {
|
|
||||||
const comp = m.components[key]
|
|
||||||
return {
|
|
||||||
path: m.path,
|
|
||||||
c: comp
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addPreFetchHooks ({ router, store, publicPath }) {
|
|
||||||
// Add router hook for handling preFetch.
|
|
||||||
// Doing it after initial route is resolved so that we don't double-fetch
|
|
||||||
// the data that we already have. Using router.beforeResolve() so that all
|
|
||||||
// async components are resolved.
|
|
||||||
router.beforeResolve((to, from, next) => {
|
|
||||||
const
|
|
||||||
urlPath = window.location.href.replace(window.location.origin, ''),
|
|
||||||
matched = getMatchedComponents(to, router),
|
|
||||||
prevMatched = getMatchedComponents(from, router)
|
|
||||||
|
|
||||||
let diffed = false
|
|
||||||
const preFetchList = matched
|
|
||||||
.filter((m, i) => {
|
|
||||||
return diffed || (diffed = (
|
|
||||||
!prevMatched[i] ||
|
|
||||||
prevMatched[i].c !== m.c ||
|
|
||||||
m.path.indexOf('/:') > -1 // does it has params?
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.filter(m => m.c !== void 0 && (
|
|
||||||
typeof m.c.preFetch === 'function'
|
|
||||||
// Class components return the component options (and the preFetch hook) inside __c property
|
|
||||||
|| (m.c.__c !== void 0 && typeof m.c.__c.preFetch === 'function')
|
|
||||||
))
|
|
||||||
.map(m => m.c.__c !== void 0 ? m.c.__c.preFetch : m.c.preFetch)
|
|
||||||
|
|
||||||
|
|
||||||
if (appPrefetch !== false) {
|
|
||||||
preFetchList.unshift(appPrefetch)
|
|
||||||
appPrefetch = false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (preFetchList.length === 0) {
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasRedirected = false
|
|
||||||
const redirect = url => {
|
|
||||||
hasRedirected = true
|
|
||||||
next(url)
|
|
||||||
}
|
|
||||||
const proceed = () => {
|
|
||||||
|
|
||||||
if (hasRedirected === false) { next() }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
preFetchList.reduce(
|
|
||||||
(promise, preFetch) => promise.then(() => hasRedirected === false && preFetch({
|
|
||||||
store,
|
|
||||||
currentRoute: to,
|
|
||||||
previousRoute: from,
|
|
||||||
redirect,
|
|
||||||
urlPath,
|
|
||||||
publicPath
|
|
||||||
})),
|
|
||||||
Promise.resolve()
|
|
||||||
)
|
|
||||||
.then(proceed)
|
|
||||||
.catch(e => {
|
|
||||||
console.error(e)
|
|
||||||
proceed()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/**
|
|
||||||
* THIS FILE IS GENERATED AUTOMATICALLY.
|
|
||||||
* DO NOT EDIT.
|
|
||||||
*
|
|
||||||
* You are probably looking on adding startup/initialization code.
|
|
||||||
* Use "quasar new boot <name>" and add it there.
|
|
||||||
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
|
|
||||||
* boot: ['file', ...] // do not add ".js" extension to it.
|
|
||||||
*
|
|
||||||
* Boot files are your "main.js"
|
|
||||||
**/
|
|
||||||
|
|
||||||
import lang from 'quasar/lang/tr.js'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import {Loading,Dialog,Notify} from 'quasar'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default { config: {"notify":{"position":"top","timeout":2500}},lang,plugins: {Loading,Dialog,Notify} }
|
|
||||||
|
|
||||||
1
ui/dist/spa/css/358.36bfde07.css
vendored
@@ -1 +0,0 @@
|
|||||||
.ol-page[data-v-68dfbebc]{padding:10px}.ol-filter-bar[data-v-68dfbebc]{margin-bottom:8px}.ol-filter-row[data-v-68dfbebc]{align-items:center;display:flex;flex-wrap:nowrap;gap:10px}.ol-filter-input[data-v-68dfbebc]{flex:0 0 136px;min-width:118px;width:136px}.ol-search[data-v-68dfbebc]{flex:1 1 360px;max-width:420px;min-width:240px}.ol-filter-actions[data-v-68dfbebc]{display:flex;flex:0 0 auto;flex-wrap:nowrap;gap:8px}.ol-filter-total[data-v-68dfbebc]{align-items:flex-end;align-self:center;display:flex;flex:0 0 auto;flex-direction:column;gap:2px;justify-content:center;line-height:1.2;margin-left:auto;min-width:250px}.ol-total-line[data-v-68dfbebc]{align-items:baseline;display:flex;gap:8px;white-space:nowrap}.ol-total-label[data-v-68dfbebc]{color:#4b5563;font-size:12px}.ol-total-value[data-v-68dfbebc]{font-size:13px}.ol-table[data-v-68dfbebc] .q-table thead th{font-size:11px;padding:4px 6px;white-space:nowrap}.ol-table[data-v-68dfbebc] .q-table tbody td{font-size:11px;padding:3px 6px}.ol-col-multiline[data-v-68dfbebc]{display:block;display:-webkit-box;line-height:1.15;overflow:hidden;text-overflow:ellipsis;white-space:normal!important;word-break:break-word;-webkit-box-orient:vertical;-webkit-line-clamp:2;max-height:2.35em}.ol-col-cari[data-v-68dfbebc]{max-width:160px}.ol-col-short[data-v-68dfbebc]{max-width:88px}.ol-col-desc[data-v-68dfbebc]{max-width:160px}.ol-pack-rate-cell[data-v-68dfbebc]{font-weight:700}.pack-rate-danger[data-v-68dfbebc]{color:#c62828}.pack-rate-warn[data-v-68dfbebc]{color:#8a6d00}.pack-rate-ok[data-v-68dfbebc]{color:#1f7a4f}@media (max-width:1440px){.ol-filter-row[data-v-68dfbebc]{align-items:flex-start;flex-wrap:wrap}.ol-filter-actions[data-v-68dfbebc]{flex-wrap:wrap}.ol-filter-input[data-v-68dfbebc]{flex:1 1 140px}.ol-filter-total[data-v-68dfbebc]{align-items:flex-start;margin-left:0;margin-top:6px;min-width:100%}.ol-total-line[data-v-68dfbebc]{justify-content:space-between;width:100%}}
|
|
||||||
1
ui/dist/spa/css/398.961173a2.css
vendored
@@ -1 +0,0 @@
|
|||||||
.bulk-close-page[data-v-734820af]{padding:10px}.bulk-filter-bar[data-v-734820af]{margin-bottom:8px}.bulk-filter-row[data-v-734820af]{align-items:flex-start;display:flex;flex-wrap:wrap;gap:10px}.bulk-search[data-v-734820af]{flex:1 1 420px;max-width:520px;min-width:320px}.bulk-filter-actions[data-v-734820af]{display:flex;flex-wrap:wrap;gap:8px}.bulk-summary[data-v-734820af]{align-items:flex-end;display:flex;flex-direction:column;font-size:12px;margin-left:auto;min-width:140px}.bulk-table[data-v-734820af] .q-table thead th{font-size:11px;font-weight:700;white-space:nowrap}.bulk-table[data-v-734820af] .q-table tbody td{font-size:11px;white-space:nowrap}.pack-rate-danger[data-v-734820af]{color:#c62828}.pack-rate-warn[data-v-734820af]{color:#8a6d00}.pack-rate-ok[data-v-734820af]{color:#1f7a4f}@media (max-width:1200px){.bulk-summary[data-v-734820af]{align-items:flex-start;margin-left:0}}
|
|
||||||
1
ui/dist/spa/css/54.b237f020.css
vendored
@@ -1 +0,0 @@
|
|||||||
.rdp-list-page[data-v-a17e51d4]{--rdp-header-h:56px;--rdp-filter-h:96px;background:#fff;display:flex;flex-direction:column;height:calc(100vh - var(--rdp-header-h));overflow:auto;padding:10px}.rdp-filter-bar[data-v-a17e51d4]{align-items:center;background:#fff;border-bottom:1px solid #ddd;box-shadow:0 1px 2px #0000000f;display:flex;margin-bottom:8px;min-height:var(--rdp-filter-h);padding:10px 12px;position:sticky;top:0;z-index:600}.rdp-filter-row[data-v-a17e51d4]{align-items:flex-end;display:flex;flex-wrap:nowrap;gap:12px}.rdp-filter-input[data-v-a17e51d4]{max-width:300px;min-width:180px}.rdp-search[data-v-a17e51d4]{flex:1 1 360px;max-width:520px;min-width:300px}.rdp-config-menus[data-v-a17e51d4],.rdp-filter-actions[data-v-a17e51d4]{align-items:center;display:flex;flex:0 0 auto;gap:8px;white-space:nowrap}.rdp-menu-list[data-v-a17e51d4]{max-height:420px;min-width:260px}.rdp-summary[data-v-a17e51d4]{align-self:center;background:#f9fafb;border:1px solid #e0e0e0;border-radius:6px;color:#4b5563;font-size:12px;margin-left:auto;padding:8px 12px;white-space:nowrap}.rdp-table[data-v-a17e51d4] .q-table__middle{max-height:none!important;overflow:visible!important}.rdp-table[data-v-a17e51d4] .q-table thead th{background:#fff;box-shadow:0 2px 4px #00000014;font-size:11px;position:sticky;top:var(--rdp-filter-h);white-space:nowrap;z-index:500}.rdp-table[data-v-a17e51d4] .q-table tbody td{font-size:11px;padding:3px 6px}.rdp-table[data-v-a17e51d4] .q-checkbox__inner{pointer-events:none}.rdp-table[data-v-a17e51d4] .freeze-col{background:#fff;position:sticky;z-index:510}.rdp-table[data-v-a17e51d4] thead .freeze-col{background:#fff;z-index:520}.rdp-table[data-v-a17e51d4] .freeze-1{left:0}.rdp-table[data-v-a17e51d4] .freeze-2{left:56px}.rdp-table[data-v-a17e51d4] .freeze-3{left:276px}@media (max-width:1400px){.rdp-filter-row[data-v-a17e51d4]{align-items:flex-start;flex-wrap:wrap}.rdp-config-menus[data-v-a17e51d4],.rdp-filter-actions[data-v-a17e51d4]{flex-wrap:wrap}.rdp-summary[data-v-a17e51d4]{margin-left:0;margin-top:6px;min-width:100%}}
|
|
||||||
1
ui/dist/spa/css/607.8d5cccce.css
vendored
@@ -1 +0,0 @@
|
|||||||
.perm-gateway[data-v-57a9abef]{padding:24px}
|
|
||||||
1
ui/dist/spa/css/784.5916f342.css
vendored
@@ -1 +0,0 @@
|
|||||||
.user-gateway-page[data-v-7b115e06]{background:#fafafa}.gateway-container[data-v-7b115e06]{max-width:900px;padding:24px;width:100%}.gateway-header[data-v-7b115e06]{text-align:center}.gateway-actions[data-v-7b115e06]{justify-content:center}.gateway-card[data-v-7b115e06]{transition:all .2s ease;width:280px}.gateway-card[data-v-7b115e06]:hover{box-shadow:0 8px 24px #00000014;transform:translateY(-4px)}
|
|
||||||
1
ui/dist/spa/css/app.53116624.css
vendored
@@ -1 +0,0 @@
|
|||||||
.with-bg{min-height:100%;position:relative}.with-bg:before{background:url(/images/Baggi-tekstilas-logolu.jpg) no-repeat top;background-size:400px auto;content:"";inset:0;opacity:.15;pointer-events:none;position:absolute;z-index:0}.with-bg>*{position:relative;z-index:1}.q-page{margin-top:5px}@media (max-width:768px){.with-bg:before{background-size:260px auto}}.filter-sticky{position:sticky;top:56px;z-index:300}.filter-collapsible,.filter-sticky{background:#fff}.table-scroll{height:calc(100vh - 56px);margin-top:0;overflow-x:auto;overflow-y:auto;position:relative}.sticky-table .q-table__middle{max-height:none!important;overflow:visible!important}.sticky-table .q-table__top{background:#fff;box-shadow:0 2px 4px #00000014;position:sticky;top:0;z-index:220}.sticky-table thead th{background:#fff;position:sticky;top:40px;z-index:210}.sticky-bar{background:#fff;border-bottom:1px solid #ddd;padding:4px 8px;position:sticky;top:0;z-index:230}.sticky-table thead th{max-width:400px;min-width:80px;overflow:auto;resize:horizontal}.sticky-table td{font-size:.95rem;font-weight:600;line-height:1.2rem;max-width:400px;min-width:80px;overflow-wrap:break-word!important;padding:4px 8px!important;white-space:normal!important;word-break:break-word!important}.baggi-ppct{display:block;margin:30px auto 0;max-width:400px;opacity:.4}.col-desc{font-size:.75rem!important;line-height:1.1rem;max-width:220px!important;min-width:180px!important;overflow-wrap:break-word;white-space:normal!important;width:220px!important;word-break:break-word!important}.custom-table{font-size:.8rem}.custom-table th{background:#fff;color:#222;font-weight:800}.custom-table td{color:#333;font-weight:600}.custom-subtable{background:#fafafa;font-size:.72rem}.custom-subtable th{background:#f9f9f9;color:#555;font-weight:500}.custom-subtable td{color:#666;font-weight:400}.col-narrow{font-size:.72rem;max-width:90px;overflow:hidden;padding:2px 6px!important;text-overflow:ellipsis;white-space:nowrap}.group-row{background:#f1f1f1!important;border-bottom:2px solid #ccc;border-top:2px solid #ccc;color:#222;font-weight:700!important}.balance-card{align-items:center;border-radius:8px;display:flex;justify-content:center;min-height:120px;width:100%}.q-table td[data-col=BELGE_NO],.q-table td[data-col=Belge_No],.q-table td[data-col=belge_no]{color:var(--q-primary)!important;font-weight:600!important}.permissions-toolbar{align-items:center;background:#fff;border-bottom:1px solid #ddd;display:flex;gap:12px;padding:8px 16px;position:sticky;top:42px;z-index:300}.permissions-table-scroll{height:calc(100vh - 112px);overflow-x:auto;overflow-y:auto;position:relative}.permissions-table .q-table__middle{max-height:none!important;overflow:auto!important;padding-top:0}.permissions-table thead th{background:#fff;box-shadow:0 2px 4px #00000014;top:10px;z-index:210}.permissions-table td{background:#fff;font-size:.95rem;line-height:1.2rem;max-width:400px;min-width:80px;overflow-wrap:break-word!important;padding:4px 8px!important;white-space:normal!important;word-break:break-word!important}.permissions-table .permissions-sticky-col{background:#fff;box-shadow:2px 0 4px #0000000a;left:0;position:sticky;z-index:205}:root{--header-h:0px;--filter-h:72px;--save-h:60px;--grid-header-h:172px;--sub-header-h:34px;--drawer-w:240px;--col-model:90px;--col-renk:80px;--col-ana:100px;--col-alt:100px;--col-aciklama:140px;--col-adet:70px;--col-fiyat:70px;--col-pb:70px;--col-tutar:70px;--grp-title-w:90px;--grp-title-gap:4px;--beden-w:44px;--beden-h:28px;--beden-count:16;--baggi-gold:#c9a227;--baggi-gold-pale:#fff9e6;--baggi-gold-light:#fff7d2;--baggi-cream:#fffef9;--baggi-gray-border:#bbb}*,:after,:before{box-sizing:border-box}body,html{height:100%;margin:0}body{background:#fff;color:#222;font-family:Inter,Segoe UI,Arial,sans-serif;font-size:14px;line-height:1.4}#q-app,.q-page-container{margin:0;padding:0}.q-layout__page{top:0!important}.order-page{background:#fff;display:flex;flex-direction:column;height:calc(100vh - var(--header-h));overflow-x:visible;overflow-y:auto}.body--drawer-left-open .q-page-container{margin-left:var(--drawer-w);width:calc(100% - var(--drawer-w))}.body--drawer-left-closed .q-page-container{margin-left:0;width:100%}.order-scroll-x{flex:1}.order-page::-webkit-scrollbar-thumb,.order-scroll-x::-webkit-scrollbar{height:8px;width:8px}.order-scroll-x::-webkit-scrollbar-thumb{background:#c0a75e;border-radius:4px}.order-scroll-x::-webkit-scrollbar-track{background:#f9f5e6}.q-header{box-shadow:0 1px 2px #00000014;position:sticky;top:0;z-index:1000}.sticky-stack{background:#fff;box-shadow:0 1px 3px #0000000d;display:flex;flex-direction:column;margin-top:0!important;position:sticky;top:var(--header-h);z-index:950}.filter-bar{background:#fafafa;margin-top:0!important;padding:12px 24px}.filter-bar,.save-toolbar{border-bottom:1px solid #ddd}.save-toolbar{align-items:center;background:var(--baggi-gold-pale);border-top:1px solid #ddd;display:flex;justify-content:space-between;padding:10px 16px;z-index:940}.save-toolbar .label{color:#6a5314;font-weight:700}.save-toolbar .value{color:#000;font-weight:700}.save-toolbar .q-btn{border-radius:6px;font-weight:600;text-transform:none}.order-grid-header{background:var(--baggi-cream);border-bottom:2px solid var(--baggi-gray-border);box-shadow:0 2px 3px #0000000d;display:grid;grid-auto-flow:column;grid-template-columns:var(--col-model) var(--col-renk) var(--col-ana) var(--col-alt) var(--col-aciklama) calc(var(--grp-title-w) + var(--grp-title-gap) + var(--beden-w)*var(--beden-count)) var(--col-adet) var(--col-fiyat) var(--col-pb) var(--col-tutar) var(--col-termin);position:sticky;top:calc(var(--header-h) + var(--filter-h) + var(--save-h));z-index:700}.order-grid-header .col-fixed{align-items:center;background:var(--baggi-gold-light);border:1px solid #aaa;display:flex;font-size:12.5px;font-weight:700;height:var(--grid-header-h);justify-content:center;transform:rotate(180deg);writing-mode:vertical-lr}.order-grid-header .aciklama-col{background:#fff9c4;border-right:2px solid #a6a6a6}.order-grid-header .beden-block{background:#fff;border:1px solid #ccc;display:flex;flex-direction:column;height:var(--grid-header-h)}.order-grid-header .grp-row{align-items:center;display:flex;height:var(--beden-h)}.order-grid-header .grp-title{font-size:12px;font-weight:700;padding-right:4px;text-align:right;width:var(--grp-title-w)}.order-grid-header .grp-body{display:grid;grid-auto-columns:var(--beden-w);grid-auto-flow:column}.order-grid-header .grp-cell.hdr{align-items:center;border:1px solid #bbb;display:flex;font-size:11.5px;font-weight:600;height:var(--beden-h);justify-content:center;width:var(--beden-w)}.order-grid-header .total-row{align-items:stretch;background:#fff59d;display:flex;justify-content:space-between}.order-grid-header .total-cell{align-items:center;background:var(--baggi-gold-pale);border-right:1px solid #bbb;display:flex;font-size:12px;font-weight:700;justify-content:center;transform:rotate(180deg);width:var(--col-adet);writing-mode:vertical-lr}.order-sub-header{align-items:center;background:linear-gradient(90deg,#fffbe9,#fff4c4 50%,#fff1b0);border-bottom:1px solid #d6c06a;border-top:1px solid #d6c06a;box-sizing:border-box;display:grid;grid-auto-flow:column;grid-template-columns:var(--col-model) var(--col-renk) var(--col-ana) var(--col-alt) var(--col-aciklama) calc(var(--grp-title-w) + var(--grp-title-gap) + var(--beden-w)*var(--beden-count)) var(--col-adet) var(--col-fiyat) var(--col-pb) var(--col-tutar) var(--col-termin);height:var(--sub-header-h);justify-items:stretch;margin-right:0!important;margin:0!important;min-height:var(--sub-header-h);padding-right:0!important;padding:0!important;position:sticky;top:calc(var(--header-h) + var(--filter-h) + var(--save-h) + var(--grid-header-h));z-index:650}:root{--col-termin:142px}.order-sub-header .sub-left{align-items:center;color:#2b1f05;display:flex;font-weight:800;grid-column:1/span 5;padding-left:6px}.order-sub-header .sub-center{align-items:center;box-sizing:border-box;display:grid;grid-auto-columns:var(--beden-w);grid-auto-flow:column;grid-column:6/7;height:100%;justify-content:start;margin-left:var(--grp-title-gap);padding-left:var(--grp-title-w);width:calc(var(--grp-title-w) + var(--grp-title-gap) + var(--beden-w)*var(--beden-count))}.order-sub-header .beden-cell{align-items:center;background:#fffdf3;border:1px solid #d8c16b;border-right:none;box-sizing:border-box;display:flex;font-size:12px;font-weight:600;height:100%;justify-content:center;width:var(--beden-w)}.order-sub-header .beden-cell:last-child{border-right:1px solid #d8c16b}.order-sub-header .sub-right{align-items:flex-end;color:#3b2f09;display:flex;flex-direction:column;font-size:13.5px;font-weight:900;grid-column:7/-1;justify-content:center;line-height:1.3;padding-right:0;text-align:right;text-transform:uppercase;transform:translateX(-60px)}.order-sub-header:hover{background:linear-gradient(90deg,#fff9cf,#fff3b0 70%,#ffe88f)}:root{--sub-header-h:60px}.order-sub-header{overflow:hidden}.order-grid-body{background:#fff;margin-top:0!important;padding-top:var(--sub-header-h);position:relative;z-index:100}.summary-row{display:grid;grid-template-columns:var(--col-model) var(--col-renk) var(--col-ana) var(--col-alt) var(--col-aciklama) calc(var(--grp-title-w) + var(--grp-title-gap) + var(--beden-w)*var(--beden-count)) var(--col-adet) var(--col-fiyat) var(--col-pb) var(--col-tutar) var(--col-termin)}.summary-row .cell{box-sizing:border-box;color:#222;font-size:13px;height:var(--beden-h)}.summary-row.row-closed{background:#f5f5f5;opacity:.55}.summary-row.row-closed:hover{background:#f5f5f5!important}.summary-row:nth-child(odd){background:#fffef9}.summary-row .grp-area{display:flex;flex-direction:column;justify-content:center;transform:translateX(calc(var(--grp-title-w) - var(--beden-w)))}.summary-row .grp-row{display:grid;grid-auto-columns:var(--beden-w);grid-auto-flow:column}.summary-row .grp-row .cell.beden{align-items:center;border:1px solid #ddd;display:flex;font-size:12px;height:var(--beden-h);justify-content:center;width:var(--beden-w)}.cell.beden.ghost{border:1px solid #0000!important;opacity:0;pointer-events:none}.summary-row .cell.adet,.summary-row .cell.fiyat,.summary-row .cell.pb,.summary-row .cell.termin,.summary-row .cell.tutar{border-left:none!important;color:#000;font-weight:600;height:100%}.summary-row .cell.tutar{border-right:none!important;justify-content:flex-end;padding-right:8px;text-align:right}.summary-row .cell.termin{align-items:center;background:#fffef9;justify-content:center;min-width:var(--col-termin)}.summary-row .cell.termin .q-input{box-sizing:border-box;max-width:142px!important;width:100%}.summary-row .cell.termin input{font-size:13px;text-align:center}.editor{background:#fffef9;border-top:1px solid #ddd;margin-top:24px;padding:16px;position:relative;z-index:50}.editor:before{background:linear-gradient(90deg,#c9a227,#e5d28b,#fff7d2);border-radius:2px;content:"";display:block;height:4px;margin-bottom:12px}.editor .q-input,.editor .q-select{font-size:14px;margin-bottom:8px}.cell.termin .termin-label{align-items:center;background:#fffef9;border-left:1px solid #ccc;box-sizing:border-box;color:#222;display:flex;font-size:13px;font-weight:600;height:100%;justify-content:center;width:100%}@media (max-width:1024px){:root{--beden-w:40px;--col-aciklama:120px}.order-grid-header .col-fixed{font-size:11px}.order-sub-header{font-size:12.5px}}@media (max-width:768px){:root{--beden-w:36px;--col-model:70px;--col-renk:60px;--col-aciklama:100px}.order-page{font-size:13px}.order-grid-header .total-cell{font-size:10.5px}}.summary-row .cell{align-items:center;display:flex;height:auto;justify-content:center;padding:4px 6px;text-align:center;white-space:normal;word-wrap:break-word}.summary-row .grp-area,.summary-row .grp-row,.summary-row .grp-row .cell.beden{align-items:center;height:100%}.summary-row .cell.aciklama{align-items:flex-start!important;background:#fff!important;border-right:1px solid #ccc!important;box-sizing:border-box!important;display:flex!important;flex-direction:column!important;font-size:13px!important;grid-column:5/6!important;justify-content:flex-start!important;line-height:1.4!important;margin-right:-92px!important;min-height:36px!important;overflow-wrap:break-word!important;padding:6px 12px!important;position:relative!important;text-align:left!important;white-space:normal!important;width:calc(var(--col-aciklama) + 92px)!important;word-break:break-word!important;z-index:10!important}.order-grid-header .col-fixed,.summary-row .cell,.summary-row .grp-row .cell.beden{border-color:#bbb!important}.summary-row .cell:not(:last-child){border-right:1px solid #bdbdbd!important}.summary-row{border-bottom:1px solid #ccc}.summary-row:last-child{border-bottom:2px solid #b7a33a}.summary-row .cell,.summary-row .grp-row .cell.beden{border-bottom:1px solid #ddd!important}.summary-row:hover{background:#fffce0}.summary-row.is-editing{background:#fff3cd;outline:2px solid #caa83f;z-index:2}.editor .q-btn:hover{background:#d2b04d;color:#fff}.summary-row:hover .cell,.summary-row:hover .grp-row .cell.beden{border-bottom:1px solid #ccc!important}.stok-red{color:#e53935;font-weight:600}.stok-yellow{color:#f9a825;font-weight:600}.stok-green{color:#43a047;font-weight:600}.q-banner.rounded-borders{border-radius:8px}.order-gateway{background:linear-gradient(145deg,#fff,#fafafa);height:100%}.order-btn{border-radius:12px;font-size:1.2rem;min-width:280px;padding:20px 40px;transition:all .2s ease}.order-btn:hover{box-shadow:0 4px 12px #00000026;transform:translateY(-3px)}.body--drawer-left-open .order-page{overflow-x:visible;width:calc(100vw - var(--drawer-w))}.order-scroll-x{box-sizing:border-box;max-width:100%}.order-grid-body,.order-grid-header,.order-sub-header{box-sizing:border-box;min-width:fit-content;width:100%}.body--drawer-left-open .filter-bar,.body--drawer-left-open .order-grid-body,.body--drawer-left-open .order-grid-header,.body--drawer-left-open .order-sub-header,.body--drawer-left-open .save-toolbar{box-sizing:border-box;margin-left:0;margin-right:0;overflow-x:hidden;width:calc(100vw - var(--drawer-w))}.body--drawer-left-closed .filter-bar,.body--drawer-left-closed .order-grid-body,.body--drawer-left-closed .order-grid-header,.body--drawer-left-closed .order-sub-header,.body--drawer-left-closed .save-toolbar{width:100vw}.order-grid-body,.order-grid-header,.order-sub-header{border-right:2px solid var(--baggi-gold)}.body--drawer-left-open .filter-bar,.body--drawer-left-open .order-grid-body,.body--drawer-left-open .order-grid-header,.body--drawer-left-open .order-page,.body--drawer-left-open .order-sub-header,.body--drawer-left-open .save-toolbar{margin-right:0!important;overflow-x:visible!important;padding-right:0!important;width:calc(100vw - var(--drawer-w) - 8px)}.order-grid-body{border-right:2px solid var(--baggi-gold)}.order-scroll-x{align-items:flex-start;background:#fff;display:flex;flex-direction:column;overflow-x:auto;overflow-y:visible}.filter-bar,.order-grid-header,.order-sub-header,.save-toolbar{box-sizing:border-box;min-width:100%;width:fit-content}.order-grid-body{box-sizing:border-box;width:fit-content}.summary-row.row-closed{opacity:.65;pointer-events:none}.summary-row.row-closed,.summary-row.row-closed:hover{background:#e6e6e6!important}.summary-row.row-closed.is-editing{outline:none!important}.filter-bar,.order-grid-body,.order-grid-header,.order-sub-header,.save-toolbar{border-right:none!important;margin-right:0!important;padding-right:0!important}.summary-row.row-error{background:#c1001514}.row-error-icon{left:4px;position:absolute;top:50%;transform:translateY(-50%)}.body--drawer-left-closed .order-scroll-x,.body--drawer-left-open .order-scroll-x{overflow-x:auto;width:100%}:root{--ol-header-h:56px;--ol-filter-h:96px}.ol-page{background:#fff;display:flex;flex-direction:column;height:calc(100vh - var(--ol-header-h));overflow:auto}.ol-filter-bar{align-items:center;box-shadow:0 1px 2px #0000000f;display:flex;min-height:var(--ol-filter-h);padding:10px 16px}.ol-table .q-table__middle{max-height:none!important;overflow:visible!important}.ol-table thead th{background:#fff;box-shadow:0 2px 4px #00000014;font-weight:700;position:sticky;top:var(--ol-filter-h);z-index:500}.ol-table .q-table__body .q-tr:nth-child(odd){background-color:#f7f7f7!important}.ol-table .q-table__body .q-tr:nth-child(2n){background-color:#fff!important}.ol-table .q-table__body .q-tr:hover{background-color:#fff7d1!important;transition:background-color .15s ease}.ol-table .q-td{font-size:.9rem;line-height:1.3;padding:6px 8px!important}.q-header{z-index:1000!important}.q-drawer{z-index:950!important}@media (max-width:768px){:root{--ol-filter-h:64px}.ol-filter-bar{padding:8px 12px}}.ol-table .q-table__body .q-tr:nth-child(odd),.ol-table tbody tr:nth-child(odd){background-color:#faf8ef!important}.ol-table .q-table__body .q-tr:nth-child(2n),.ol-table tbody tr:nth-child(2n){background-color:#fff!important}.ol-table .q-table__body .q-tr:hover,.ol-table tbody tr:hover{background-color:#fff4cc!important;transition:background-color .2s ease}.ol-qbanner{background:#f9fafb;border:1px solid #e0e0e0;border-radius:6px;padding:8px 12px}.ol-qbanner-amount{color:#1976d2;margin-left:6px}.ol-col-multiline{line-height:1.25rem;overflow:hidden;white-space:normal!important;word-break:break-word}.ol-col-cari{font-size:.88rem;font-weight:600;max-width:200px;min-width:150px}.ol-col-cari,.ol-col-desc{display:-webkit-box;-webkit-box-orient:vertical}.ol-col-desc{color:#444;font-size:.82rem;max-width:220px;min-width:160px}.ol-table .q-td.ol-col-cari,.ol-table .q-td.ol-col-desc{padding-bottom:6px!important;padding-top:6px!important}.ol-table th.ol-col-cari,.ol-table th.ol-col-desc{white-space:nowrap}.ol-filter-bar{background:#fff;border-bottom:1px solid #ddd;padding:10px 12px;position:sticky;top:0;z-index:600}.ol-filter-row{align-items:flex-end;display:flex;flex-wrap:nowrap;gap:12px}.ol-filter-input{max-width:260px;min-width:180px}.ol-search{min-width:280px}.ol-filter-actions{align-items:center;display:flex;gap:8px;white-space:nowrap}.ol-filter-total{background:#f9fafb;border:1px solid #e0e0e0;border-radius:6px;margin-left:auto;padding:8px 12px;white-space:nowrap}@media (max-width:1200px){.ol-filter-row{flex-wrap:wrap;row-gap:8px}.ol-filter-total{justify-content:flex-end;width:100%}}.order-gateway{min-height:100vh}.draft-card{max-width:90vw;width:320px}.act-page{background:#fff;height:calc(100vh - 56px);overflow:auto}.act-filter-bar{background:#fff;border-bottom:1px solid #ddd;box-shadow:0 1px 2px #0000000f;padding:10px 16px;position:sticky;top:0;z-index:620}.act-filter-row{align-items:flex-end;display:flex;flex-wrap:nowrap;gap:12px}.act-filter-input{max-width:240px;min-width:160px}.act-filter-wide{min-width:260px}.act-filter-actions{display:flex;gap:8px;margin-left:auto;white-space:nowrap}.act-table{font-size:.85rem}.act-table thead th{background:#fff;box-shadow:0 2px 4px #00000014;font-weight:700;position:sticky;top:56px;z-index:500}.act-table tbody tr:nth-child(odd){background:#faf8ef}.act-table tbody tr:nth-child(2n){background:#fff}.act-table tbody tr:hover{background:#fff4cc}.act-table .q-td{font-weight:600;line-height:1.25;padding:6px 8px!important}.act-row-success{background:#43a0470f}.act-row-fail{background:#d32f2f0f}.act-badge-ok{background:#43a047}.act-badge-fail{background:#e53935}.act-col-narrow{max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.act-col-route{font-size:.8rem;max-width:260px;white-space:normal;word-break:break-word}.act-col-meta{color:#555;font-size:.75rem}.ol-col-piyasa{padding-bottom:6px!important;padding-top:6px!important;vertical-align:top}.piyasa-wrap{align-content:flex-start;column-gap:6px;display:flex;flex-wrap:wrap;max-height:none;overflow:visible;row-gap:4px}.piyasa-chip{flex:0 0 calc(25% - 6px);font-size:11px;font-weight:600;line-height:1.1;max-width:calc(25% - 6px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.user-detail-page{background:#fafafa}.image-preview{border-radius:6px;width:100%}.image-thumb{border-radius:4px;width:100%}.workorder-page{padding-bottom:80px}.permissions-page{background:#fff;display:flex;flex-direction:column;height:calc(100vh - 56px)}.permissions-table-scroll{background:#fff;flex:1;overflow:auto}.permissions-table{font-size:.85rem}.permissions-table thead th{background:var(--baggi-cream);box-shadow:0 2px 3px #0000000f;color:#222;font-weight:800;position:sticky;top:0;z-index:300}.permissions-table td{color:#333;font-weight:600;padding:6px 8px!important}.permissions-table tbody tr:nth-child(odd){background:#fffef7}.permissions-table tbody tr:nth-child(2n){background:#fff}.permissions-table tbody tr:hover{background:#fff4cc}.permissions-sticky-col{background:#fff;box-shadow:2px 0 4px #0000000a;font-weight:700;left:0;position:sticky;z-index:250}.permissions-table .q-th .column{gap:2px}
|
|
||||||
BIN
ui/dist/spa/css/app.53116624.css.gz
vendored
6
ui/dist/spa/css/vendor.724dcfab.css
vendored
BIN
ui/dist/spa/css/vendor.724dcfab.css.gz
vendored
BIN
ui/dist/spa/favicon.ico
vendored
|
Before Width: | Height: | Size: 33 KiB |
BIN
ui/dist/spa/icons/baggi-icon.png
vendored
|
Before Width: | Height: | Size: 145 KiB |
BIN
ui/dist/spa/icons/favicon-128x128.png
vendored
|
Before Width: | Height: | Size: 3.0 KiB |
BIN
ui/dist/spa/icons/favicon-16x16.png
vendored
|
Before Width: | Height: | Size: 1.2 KiB |
BIN
ui/dist/spa/icons/favicon-32x32.png
vendored
|
Before Width: | Height: | Size: 1.3 KiB |
BIN
ui/dist/spa/icons/favicon-96x96.png
vendored
|
Before Width: | Height: | Size: 2.3 KiB |
BIN
ui/dist/spa/images/Baggi-Exclusive-Logo.jpg
vendored
|
Before Width: | Height: | Size: 364 KiB |
BIN
ui/dist/spa/images/Baggi-Fabrika-resmi.jpg
vendored
|
Before Width: | Height: | Size: 117 KiB |
BIN
ui/dist/spa/images/Baggi-tekstilas-logolu.jpg
vendored
|
Before Width: | Height: | Size: 142 KiB |
1
ui/dist/spa/index.html
vendored
@@ -1 +0,0 @@
|
|||||||
<!DOCTYPE html><html><head><title>Baggi SS</title><meta charset=utf-8><meta name=description content="A Quasar Project"><meta name=format-detection content="telephone=no"><meta name=msapplication-tap-highlight content=no><meta name=viewport content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width"><link rel=icon type=image/png sizes=128x128 href=/icons/favicon-128x128.png><link rel=icon type=image/png sizes=96x96 href=/icons/favicon-96x96.png><link rel=icon type=image/png sizes=32x32 href=/icons/favicon-32x32.png><link rel=icon type=image/png sizes=16x16 href=/icons/favicon-16x16.png><link rel=icon type=image/ico href=/favicon.ico><script defer src=/js/vendor.9ea1812a.js></script><script defer src=/js/app.bcfe0c60.js></script><link href=/css/vendor.724dcfab.css rel=stylesheet><link href=/css/app.53116624.css rel=stylesheet></head><body><div id=q-app></div></body></html>
|
|
||||||
10
ui/package-lock.json
generated
@@ -7,7 +7,6 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "baggisowtfaresystem",
|
"name": "baggisowtfaresystem",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"hasInstallScript": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@quasar/extras": "^1.16.4",
|
"@quasar/extras": "^1.16.4",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
@@ -20,7 +19,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@quasar/app-webpack": "^4.1.0",
|
"@quasar/app-webpack": "^4.1.0",
|
||||||
"autoprefixer": "^10.4.2"
|
"autoprefixer": "^10.4.2",
|
||||||
|
"baseline-browser-mapping": "^2.9.19"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^28 || ^26 || ^24 || ^22 || ^20 || ^18",
|
"node": "^28 || ^26 || ^24 || ^22 || ^20 || ^18",
|
||||||
@@ -3856,9 +3856,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.8.12",
|
"version": "2.9.19",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.12.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
|
||||||
"integrity": "sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==",
|
"integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -23,7 +23,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@quasar/app-webpack": "^4.1.0",
|
"@quasar/app-webpack": "^4.1.0",
|
||||||
"autoprefixer": "^10.4.2"
|
"autoprefixer": "^10.4.2",
|
||||||
|
"baseline-browser-mapping": "^2.9.19"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"last 10 Chrome versions",
|
"last 10 Chrome versions",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
import { defineConfig } from '#q-app/wrappers'
|
import { defineConfig } from '#q-app/wrappers'
|
||||||
|
|
||||||
export default defineConfig(() => {
|
export default defineConfig(() => {
|
||||||
|
const apiBaseUrl = (process.env.VITE_API_BASE_URL || '/api').trim()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
||||||
/* =====================================================
|
/* =====================================================
|
||||||
@@ -33,6 +35,9 @@ export default defineConfig(() => {
|
|||||||
===================================================== */
|
===================================================== */
|
||||||
build: {
|
build: {
|
||||||
vueRouterMode: 'hash',
|
vueRouterMode: 'hash',
|
||||||
|
env: {
|
||||||
|
VITE_API_BASE_URL: apiBaseUrl
|
||||||
|
},
|
||||||
|
|
||||||
esbuildTarget: {
|
esbuildTarget: {
|
||||||
browser: ['es2022', 'firefox115', 'chrome115', 'safari14'],
|
browser: ['es2022', 'firefox115', 'chrome115', 'safari14'],
|
||||||
@@ -52,14 +57,15 @@ export default defineConfig(() => {
|
|||||||
port: 9000,
|
port: 9000,
|
||||||
open: true,
|
open: true,
|
||||||
|
|
||||||
// DEV proxy (CORS’suz)
|
// DEV proxy (CORS'suz)
|
||||||
proxy: {
|
proxy: [
|
||||||
'/api': {
|
{
|
||||||
|
context: ['/api'],
|
||||||
target: 'http://localhost:8080',
|
target: 'http://localhost:8080',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false
|
secure: false
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
/* =====================================================
|
/* =====================================================
|
||||||
@@ -119,3 +125,4 @@ export default defineConfig(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/**
|
|
||||||
* THIS FILE IS GENERATED AUTOMATICALLY.
|
|
||||||
* 1. DO NOT edit this file directly as it won't do anything.
|
|
||||||
* 2. EDIT the original quasar.config file INSTEAD.
|
|
||||||
* 3. DO NOT git commit this file. It should be ignored.
|
|
||||||
*
|
|
||||||
* This file is still here because there was an error in
|
|
||||||
* the original quasar.config file and this allows you to
|
|
||||||
* investigate the Node.js stack error.
|
|
||||||
*
|
|
||||||
* After you fix the original file, this file will be
|
|
||||||
* deleted automatically.
|
|
||||||
**/
|
|
||||||
|
|
||||||
|
|
||||||
// quasar.config.js
|
|
||||||
import { defineConfig } from "@quasar/app-webpack/wrappers";
|
|
||||||
var quasar_config_default = defineConfig(() => {
|
|
||||||
return {
|
|
||||||
// ✅ UYGULAMA KİMLİĞİ (WEB'DE GÖRÜNEN İSİM)
|
|
||||||
productName: "Baggi BSS",
|
|
||||||
productDescription: "Baggi Tekstil Business Support System",
|
|
||||||
// 🔹 Boot dosyaları
|
|
||||||
boot: ["axios", "dayjs"],
|
|
||||||
// 🔹 Global CSS
|
|
||||||
css: ["app.css"],
|
|
||||||
// 🔹 Ekstra icon/font setleri
|
|
||||||
extras: [
|
|
||||||
"roboto-font",
|
|
||||||
"material-icons"
|
|
||||||
],
|
|
||||||
// 🔹 Derleme Ayarları
|
|
||||||
build: {
|
|
||||||
vueRouterMode: "hash",
|
|
||||||
env: {
|
|
||||||
VITE_API_BASE_URL: "/api"
|
|
||||||
},
|
|
||||||
esbuildTarget: {
|
|
||||||
browser: ["es2022", "firefox115", "chrome115", "safari14"],
|
|
||||||
node: "node20"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 🔹 Geliştirme Sunucusu
|
|
||||||
devServer: {
|
|
||||||
server: { type: "http" },
|
|
||||||
port: 9e3,
|
|
||||||
open: true
|
|
||||||
},
|
|
||||||
// 🔹 Quasar Framework ayarları
|
|
||||||
framework: {
|
|
||||||
config: {
|
|
||||||
notify: { position: "top", timeout: 2500 }
|
|
||||||
},
|
|
||||||
lang: "tr",
|
|
||||||
plugins: ["Loading", "Dialog", "Notify"]
|
|
||||||
},
|
|
||||||
animations: [],
|
|
||||||
ssr: {
|
|
||||||
prodPort: 3e3,
|
|
||||||
middlewares: ["render"],
|
|
||||||
pwa: false
|
|
||||||
},
|
|
||||||
pwa: {
|
|
||||||
workboxMode: "GenerateSW"
|
|
||||||
},
|
|
||||||
capacitor: {
|
|
||||||
hideSplashscreen: true
|
|
||||||
},
|
|
||||||
electron: {
|
|
||||||
preloadScripts: ["electron-preload"],
|
|
||||||
inspectPort: 5858,
|
|
||||||
bundler: "packager",
|
|
||||||
builder: { appId: "baggisowtfaresystem" }
|
|
||||||
},
|
|
||||||
bex: {
|
|
||||||
extraScripts: []
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
export {
|
|
||||||
quasar_config_default as default
|
|
||||||
};
|
|
||||||
125
ui/quasar.config.js.temporary.compiled.1771579736454.mjs
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* THIS FILE IS GENERATED AUTOMATICALLY.
|
||||||
|
* 1. DO NOT edit this file directly as it won't do anything.
|
||||||
|
* 2. EDIT the original quasar.config file INSTEAD.
|
||||||
|
* 3. DO NOT git commit this file. It should be ignored.
|
||||||
|
*
|
||||||
|
* This file is still here because there was an error in
|
||||||
|
* the original quasar.config file and this allows you to
|
||||||
|
* investigate the Node.js stack error.
|
||||||
|
*
|
||||||
|
* After you fix the original file, this file will be
|
||||||
|
* deleted automatically.
|
||||||
|
**/
|
||||||
|
|
||||||
|
|
||||||
|
// quasar.config.js
|
||||||
|
import { defineConfig } from "@quasar/app-webpack/wrappers";
|
||||||
|
var quasar_config_default = defineConfig(() => {
|
||||||
|
const apiBaseUrl = (process.env.VITE_API_BASE_URL || "/api").trim();
|
||||||
|
return {
|
||||||
|
/* =====================================================
|
||||||
|
APP INFO
|
||||||
|
===================================================== */
|
||||||
|
productName: "Baggi BSS",
|
||||||
|
productDescription: "Baggi Tekstil Business Support System",
|
||||||
|
/* =====================================================
|
||||||
|
BOOT FILES
|
||||||
|
===================================================== */
|
||||||
|
boot: ["dayjs"],
|
||||||
|
/* =====================================================
|
||||||
|
GLOBAL CSS
|
||||||
|
===================================================== */
|
||||||
|
css: ["app.css"],
|
||||||
|
/* =====================================================
|
||||||
|
ICONS / FONTS
|
||||||
|
===================================================== */
|
||||||
|
extras: [
|
||||||
|
"roboto-font",
|
||||||
|
"material-icons"
|
||||||
|
],
|
||||||
|
/* =====================================================
|
||||||
|
BUILD (PRODUCTION)
|
||||||
|
===================================================== */
|
||||||
|
build: {
|
||||||
|
vueRouterMode: "hash",
|
||||||
|
env: {
|
||||||
|
VITE_API_BASE_URL: apiBaseUrl
|
||||||
|
},
|
||||||
|
esbuildTarget: {
|
||||||
|
browser: ["es2022", "firefox115", "chrome115", "safari14"],
|
||||||
|
node: "node20"
|
||||||
|
},
|
||||||
|
// Cache & performance
|
||||||
|
gzip: true,
|
||||||
|
preloadChunks: true
|
||||||
|
},
|
||||||
|
/* =====================================================
|
||||||
|
DEV SERVER (LOCAL)
|
||||||
|
===================================================== */
|
||||||
|
devServer: {
|
||||||
|
server: { type: "http" },
|
||||||
|
port: 9e3,
|
||||||
|
open: true,
|
||||||
|
// DEV proxy (CORS'suz)
|
||||||
|
proxy: [
|
||||||
|
{
|
||||||
|
context: ["/api"],
|
||||||
|
target: "http://localhost:8080",
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
/* =====================================================
|
||||||
|
QUASAR FRAMEWORK
|
||||||
|
===================================================== */
|
||||||
|
framework: {
|
||||||
|
config: {
|
||||||
|
notify: {
|
||||||
|
position: "top",
|
||||||
|
timeout: 2500
|
||||||
|
}
|
||||||
|
},
|
||||||
|
lang: "tr",
|
||||||
|
plugins: [
|
||||||
|
"Loading",
|
||||||
|
"Dialog",
|
||||||
|
"Notify"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
animations: [],
|
||||||
|
/* =====================================================
|
||||||
|
SSR / PWA (DISABLED)
|
||||||
|
===================================================== */
|
||||||
|
ssr: {
|
||||||
|
prodPort: 3e3,
|
||||||
|
middlewares: ["render"],
|
||||||
|
pwa: false
|
||||||
|
},
|
||||||
|
pwa: {
|
||||||
|
workboxMode: "GenerateSW"
|
||||||
|
},
|
||||||
|
/* =====================================================
|
||||||
|
MOBILE / DESKTOP
|
||||||
|
===================================================== */
|
||||||
|
capacitor: {
|
||||||
|
hideSplashscreen: true
|
||||||
|
},
|
||||||
|
electron: {
|
||||||
|
preloadScripts: ["electron-preload"],
|
||||||
|
inspectPort: 5858,
|
||||||
|
bundler: "packager",
|
||||||
|
builder: {
|
||||||
|
appId: "baggisowtfaresystem"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bex: {
|
||||||
|
extraScripts: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
export {
|
||||||
|
quasar_config_default as default
|
||||||
|
};
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.q-page {
|
.q-page {
|
||||||
margin-top: 5px;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
@@ -288,6 +288,11 @@ body {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: visible;
|
overflow-x: visible;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
padding-top: 0 !important;
|
||||||
|
}
|
||||||
|
/* Quasar header offsetunu sadece order sayfasında sıfırla */
|
||||||
|
.q-page-container .order-page {
|
||||||
|
margin-top: calc(-1 * var(--header-h) - 58px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.body--drawer-left-open .q-page-container {
|
.body--drawer-left-open .q-page-container {
|
||||||
@@ -344,16 +349,33 @@ body {
|
|||||||
.filter-bar {
|
.filter-bar {
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
padding: 12px 24px;
|
padding: 8px 12px;
|
||||||
margin-top:0 !important;
|
margin-top:0 !important;
|
||||||
}
|
}
|
||||||
|
.filter-bar .q-field__label {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 1.1;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.filter-bar .q-field__control,
|
||||||
|
.filter-bar .q-field__native,
|
||||||
|
.filter-bar .q-field__marginal {
|
||||||
|
min-height: 36px;
|
||||||
|
}
|
||||||
|
.filter-bar-desc {
|
||||||
|
padding: 0 12px 6px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
/* 🔹 Save toolbar */
|
/* 🔹 Save toolbar */
|
||||||
.save-toolbar {
|
.save-toolbar {
|
||||||
background: var(--baggi-gold-pale);
|
background: var(--baggi-gold-pale);
|
||||||
border-top: 1px solid #ddd;
|
border-top: 1px solid #ddd;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
padding: 10px 16px;
|
padding: 6px 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -454,8 +476,9 @@ body {
|
|||||||
.order-grid-header .total-row {
|
.order-grid-header .total-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
background: #fff59d;
|
background: #fff59d;
|
||||||
|
grid-column: 7 / -1;
|
||||||
}
|
}
|
||||||
.order-grid-header .total-cell {
|
.order-grid-header .total-cell {
|
||||||
width: var(--col-adet);
|
width: var(--col-adet);
|
||||||
@@ -469,6 +492,9 @@ body {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
.order-grid-header .total-cell:last-child {
|
||||||
|
width: var(--col-termin);
|
||||||
|
}
|
||||||
/* ===========================================================
|
/* ===========================================================
|
||||||
6️⃣ SUB-HEADER (ÜRÜN GRUBU BAR) — TAM HİZALANMIŞ
|
6️⃣ SUB-HEADER (ÜRÜN GRUBU BAR) — TAM HİZALANMIŞ
|
||||||
=========================================================== */
|
=========================================================== */
|
||||||
@@ -614,6 +640,108 @@ body {
|
|||||||
padding-top: var(--sub-header-h);
|
padding-top: var(--sub-header-h);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
.order-scroll-y.compact-grid-header {
|
||||||
|
--grid-header-h: var(--beden-h);
|
||||||
|
}
|
||||||
|
.order-scroll-y.compact-grid-header .order-grid-header .col-fixed {
|
||||||
|
writing-mode: horizontal-tb;
|
||||||
|
transform: none;
|
||||||
|
height: var(--grid-header-h);
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
.order-scroll-y.compact-grid-header .order-grid-header .total-cell {
|
||||||
|
writing-mode: horizontal-tb;
|
||||||
|
transform: none;
|
||||||
|
height: var(--grid-header-h);
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
.order-grid-header.compact {
|
||||||
|
height: var(--beden-h);
|
||||||
|
}
|
||||||
|
.order-grid-header.compact .beden-block {
|
||||||
|
height: var(--beden-h);
|
||||||
|
}
|
||||||
|
.order-grid-header.compact .col-fixed,
|
||||||
|
.order-grid-header.compact .total-cell {
|
||||||
|
writing-mode: horizontal-tb;
|
||||||
|
transform: none;
|
||||||
|
height: var(--beden-h);
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
.order-grid-header.compact .total-cell {
|
||||||
|
width: var(--col-adet);
|
||||||
|
}
|
||||||
|
.order-grid-header.compact .total-cell:nth-child(2) {
|
||||||
|
width: var(--col-fiyat);
|
||||||
|
}
|
||||||
|
.order-grid-header.compact .total-cell:nth-child(3) {
|
||||||
|
width: var(--col-pb);
|
||||||
|
}
|
||||||
|
.order-grid-header.compact .total-cell:nth-child(4) {
|
||||||
|
width: var(--col-tutar);
|
||||||
|
}
|
||||||
|
.order-grid-header.compact .total-cell:nth-child(5) {
|
||||||
|
width: var(--col-termin);
|
||||||
|
}
|
||||||
|
.order-grid-header.compact .grp-row {
|
||||||
|
height: var(--beden-h);
|
||||||
|
}
|
||||||
|
.order-grid-header.compact .grp-row:not(:first-child) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.order-grid-header.compact .grp-title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.order-grid-header.compact .grp-row:first-child .grp-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
.order-grid-header.compact .grp-body {
|
||||||
|
height: var(--beden-h);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.order-grid-header.compact .grp-cell.hdr {
|
||||||
|
height: var(--beden-h);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.order-scroll-y.compact-grid-header .order-grid-header .grp-title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.order-scroll-y.compact-grid-header .order-grid-header .grp-row {
|
||||||
|
align-items: center;
|
||||||
|
height: var(--beden-h);
|
||||||
|
}
|
||||||
|
.order-scroll-y.compact-grid-header .order-grid-header .grp-body {
|
||||||
|
grid-template-rows: var(--beden-h);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.order-scroll-y.compact-grid-header .order-grid-header .grp-cell.hdr {
|
||||||
|
height: var(--beden-h);
|
||||||
|
}
|
||||||
|
.order-scroll-y.compact-grid-header .order-grid-header .total-row {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.order-scroll-y.compact-grid-header .order-grid-body {
|
||||||
|
padding-top: var(--sub-header-h);
|
||||||
|
}
|
||||||
|
|
||||||
.summary-row {
|
.summary-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1086,7 +1214,7 @@ body {
|
|||||||
z-index: 600;
|
z-index: 600;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
padding: 10px 16px;
|
padding: 6px 10px;
|
||||||
box-shadow: 0 1px 2px rgba(0,0,0,0.06);
|
box-shadow: 0 1px 2px rgba(0,0,0,0.06);
|
||||||
min-height: var(--ol-filter-h);
|
min-height: var(--ol-filter-h);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1309,7 +1437,7 @@ body {
|
|||||||
z-index: 620;
|
z-index: 620;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
padding: 10px 16px;
|
padding: 6px 10px;
|
||||||
box-shadow: 0 1px 2px rgba(0,0,0,0.06);
|
box-shadow: 0 1px 2px rgba(0,0,0,0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1530,3 +1658,21 @@ body {
|
|||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===========================================================
|
||||||
|
ORDER ENTRY POPUP EDITOR
|
||||||
|
=========================================================== */
|
||||||
|
.order-editor-card {
|
||||||
|
width: 98vw;
|
||||||
|
max-width: 1900px;
|
||||||
|
}
|
||||||
|
.order-editor-card .editor {
|
||||||
|
max-height: 76vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.order-editor-dialog .q-dialog__inner--minimized {
|
||||||
|
max-width: 98vw;
|
||||||
|
}
|
||||||
|
.order-editor-dialog .q-dialog__inner > div {
|
||||||
|
width: 98vw;
|
||||||
|
max-width: 1900px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -209,6 +209,11 @@ const menuItems = [
|
|||||||
to: '/app/order-gateway',
|
to: '/app/order-gateway',
|
||||||
permission: 'order:view'
|
permission: 'order:view'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Üretime Verilen Siparişleri Güncelle',
|
||||||
|
to: '/app/orderproductionupdate',
|
||||||
|
permission: 'order:update'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Tamamlanan Siparişleri Toplu Kapatma',
|
label: 'Tamamlanan Siparişleri Toplu Kapatma',
|
||||||
to: '/app/order-bulk-close',
|
to: '/app/order-bulk-close',
|
||||||
@@ -226,25 +231,25 @@ const menuItems = [
|
|||||||
{
|
{
|
||||||
label: 'Rol + Departman Yetkileri',
|
label: 'Rol + Departman Yetkileri',
|
||||||
to: '/app/role-dept-permissions',
|
to: '/app/role-dept-permissions',
|
||||||
permission: 'user:update'
|
permission: 'system:update'
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
label: 'Kullanıcı Yetkileri',
|
label: 'Kullanıcı Yetkileri',
|
||||||
to: '/app/user-permissions',
|
to: '/app/user-permissions',
|
||||||
permission: 'user:update'
|
permission: 'system:update'
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
label: 'Loglar',
|
label: 'Loglar',
|
||||||
to: '/app/activity-logs',
|
to: '/app/activity-logs',
|
||||||
permission: 'user:view'
|
permission: 'system:read'
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
label: 'Test Mail',
|
label: 'Test Mail',
|
||||||
to: '/app/test-mail',
|
to: '/app/test-mail',
|
||||||
permission: 'user:insert'
|
permission: 'system:update'
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
@@ -258,7 +263,7 @@ const menuItems = [
|
|||||||
{
|
{
|
||||||
label: 'Kullanıcılar',
|
label: 'Kullanıcılar',
|
||||||
to: '/app/users',
|
to: '/app/users',
|
||||||
permission: 'user:view'
|
permission: 'system:read'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- ===========================================================
|
<!-- ===========================================================
|
||||||
🧾 ORDER ENTRY PAGE (BSSApp)
|
🧾 ORDER ENTRY PAGE (BSSApp)
|
||||||
v23 — Sticky-stack + Drawer uyumlu yapı
|
v23 — Sticky-stack + Drawer uyumlu yapı
|
||||||
@@ -8,10 +8,11 @@
|
|||||||
class="order-page"
|
class="order-page"
|
||||||
>
|
>
|
||||||
<!-- 🔄 SAYFA LOADERI -->
|
<!-- 🔄 SAYFA LOADERI -->
|
||||||
<q-inner-loading :showing="loadingHeader || loadingCari || loadingModels" color="primary">
|
<q-inner-loading :showing="isPageBlocking" color="primary">
|
||||||
<q-spinner size="50px" />
|
<q-spinner size="50px" />
|
||||||
</q-inner-loading>
|
</q-inner-loading>
|
||||||
|
|
||||||
|
<template v-if="!isPageBlocking">
|
||||||
<!-- =======================================================
|
<!-- =======================================================
|
||||||
🔹 STICKY STACK (Filter + Save + Header)
|
🔹 STICKY STACK (Filter + Save + Header)
|
||||||
======================================================== -->
|
======================================================== -->
|
||||||
@@ -168,8 +169,22 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- 📝 Sipariş Genel Açıklaması (filter bar altında) -->
|
||||||
|
<div class="filter-bar-desc q-mt-sm">
|
||||||
|
<q-input
|
||||||
|
v-model="form.Description"
|
||||||
|
type="textarea"
|
||||||
|
label="Sipariş Genel Açıklaması"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
autogrow
|
||||||
|
maxlength="1500"
|
||||||
|
counter
|
||||||
|
placeholder="Siparişe genel açıklama giriniz (örn. teslimat, üretim notu, müşteri isteği...)"
|
||||||
|
:disable="isClosedRow"
|
||||||
|
:readonly="isViewOnly"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 🔹 Cari Bilgi Barı -->
|
<!-- 🔹 Cari Bilgi Barı -->
|
||||||
<q-slide-transition>
|
<q-slide-transition>
|
||||||
@@ -212,6 +227,14 @@
|
|||||||
<div class="save-toolbar">
|
<div class="save-toolbar">
|
||||||
<div class="text-subtitle2 text-weight-bold">Sipariş Formu</div>
|
<div class="text-subtitle2 text-weight-bold">Sipariş Formu</div>
|
||||||
<div>
|
<div>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
color="grey-7"
|
||||||
|
class="q-ml-sm"
|
||||||
|
:label="compactGridHeader ? 'BAŞLIK GENİŞLET' : 'BAŞLIK DARALT'"
|
||||||
|
:icon="compactGridHeader ? 'unfold_more' : 'unfold_less'"
|
||||||
|
@click="compactGridHeader = !compactGridHeader"
|
||||||
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="isViewOnly && canExportOrder"
|
v-if="isViewOnly && canExportOrder"
|
||||||
label="🖨 SİPARİŞİ YAZDIR"
|
label="🖨 SİPARİŞİ YAZDIR"
|
||||||
@@ -222,7 +245,16 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
v-else-if="canSubmitOrder"
|
v-if="canMutateRows"
|
||||||
|
label="SATIR EKLE"
|
||||||
|
color="secondary"
|
||||||
|
icon="add"
|
||||||
|
class="q-ml-sm"
|
||||||
|
@click="openNewRowEditor"
|
||||||
|
:disable="isClosedRow || isViewOnly || !canMutateRows"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
v-if="canSubmitOrder"
|
||||||
:label="isEditMode ? 'TÜMÜNÜ GÜNCELLE' : 'TÜMÜNÜ KAYDET'"
|
:label="isEditMode ? 'TÜMÜNÜ GÜNCELLE' : 'TÜMÜNÜ KAYDET'"
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="save"
|
icon="save"
|
||||||
@@ -231,24 +263,15 @@
|
|||||||
:disable="!canSubmitOrder"
|
:disable="!canSubmitOrder"
|
||||||
@click="confirmAndSubmit"
|
@click="confirmAndSubmit"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
|
||||||
label="YENİ SİPARİŞ"
|
|
||||||
v-if="canWriteOrder"
|
|
||||||
color="secondary"
|
|
||||||
icon="add_circle"
|
|
||||||
class="q-ml-sm"
|
|
||||||
@click="onResetEditorClick"
|
|
||||||
:disable="isClosedRow || !canWriteOrder"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 🔹 Grid Header -->
|
<!-- 🔹 Grid Header -->
|
||||||
<div class="order-grid-header">
|
<div class="order-grid-header" :class="{ compact: compactGridHeader }">
|
||||||
<div class="col-fixed model">MODEL</div>
|
<div class="col-fixed model">MODEL</div>
|
||||||
<div class="col-fixed renk">RENK</div>
|
<div class="col-fixed renk">RENK</div>
|
||||||
<div class="col-fixed ana">ÜRÜN ANA<br />GRUBU</div>
|
<div class="col-fixed ana">ÜRÜN ANA GRUBU</div>
|
||||||
<div class="col-fixed alt">ÜRÜN ALT<br />GRUBU</div>
|
<div class="col-fixed alt">ÜRÜN ALT GRUBU</div>
|
||||||
<div class="col-fixed aciklama-col">AÇIKLAMA</div>
|
<div class="col-fixed aciklama-col">AÇIKLAMA</div>
|
||||||
|
|
||||||
<div class="beden-block">
|
<div class="beden-block">
|
||||||
@@ -289,7 +312,7 @@
|
|||||||
<!-- =======================================================
|
<!-- =======================================================
|
||||||
🔹 GRID BODY (Final Stabil) + EDITOR aynı scroll’da
|
🔹 GRID BODY (Final Stabil) + EDITOR aynı scroll’da
|
||||||
======================================================== -->
|
======================================================== -->
|
||||||
<div class="order-scroll-y"> <!-- ✅ YENİ: Grid + Editor ortak dikey scroll -->
|
<div class="order-scroll-y" :class="{ 'compact-grid-header': compactGridHeader }"> <!-- ✅ YENİ: Grid + Editor ortak dikey scroll -->
|
||||||
<div class="order-grid-body">
|
<div class="order-grid-body">
|
||||||
<template v-for="grp in groupedRows" :key="grp.name">
|
<template v-for="grp in groupedRows" :key="grp.name">
|
||||||
<div :class="['summary-group', grp.open ? 'open' : 'closed']">
|
<div :class="['summary-group', grp.open ? 'open' : 'closed']">
|
||||||
@@ -424,7 +447,24 @@
|
|||||||
<!-- =======================================================
|
<!-- =======================================================
|
||||||
🔹 SATIR DÜZENLEYİCİ FORM (EDITOR)
|
🔹 SATIR DÜZENLEYİCİ FORM (EDITOR)
|
||||||
======================================================== -->
|
======================================================== -->
|
||||||
<div class="editor q-mt-lg q-pa-sm">
|
<q-dialog
|
||||||
|
v-model="showEditor"
|
||||||
|
class="order-editor-dialog"
|
||||||
|
:maximized="$q.screen.lt.md"
|
||||||
|
full-width
|
||||||
|
transition-show="jump-down"
|
||||||
|
transition-hide="jump-up"
|
||||||
|
persistent
|
||||||
|
>
|
||||||
|
<q-card class="order-editor-card">
|
||||||
|
<q-card-section class="row items-center justify-between">
|
||||||
|
<div class="text-subtitle1 text-weight-bold">
|
||||||
|
{{ isEditing ? 'Satır Düzenle' : 'Yeni Satır' }}
|
||||||
|
</div>
|
||||||
|
<q-btn flat round icon="close" @click="showEditor = false" />
|
||||||
|
</q-card-section>
|
||||||
|
<q-separator />
|
||||||
|
<q-card-section class="editor q-pa-sm">
|
||||||
|
|
||||||
<!-- 🔸 1. Satır: Model ve Ürün Bilgileri -->
|
<!-- 🔸 1. Satır: Model ve Ürün Bilgileri -->
|
||||||
<div class="row q-col-gutter-sm q-mb-sm">
|
<div class="row q-col-gutter-sm q-mb-sm">
|
||||||
@@ -717,28 +757,11 @@
|
|||||||
butonuna basarak işlemleri kaydedebilirsiniz.
|
butonuna basarak işlemleri kaydedebilirsiniz.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- =======================================================
|
</q-card-section>
|
||||||
🔹 SİPARİŞ GENEL AÇIKLAMASI
|
</q-card>
|
||||||
======================================================== -->
|
</q-dialog>
|
||||||
<div class="row q-mt-md">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-input
|
|
||||||
v-model="form.Description"
|
|
||||||
type="textarea"
|
|
||||||
label="Sipariş Genel Açıklaması"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
autogrow
|
|
||||||
maxlength="1500"
|
|
||||||
counter
|
|
||||||
placeholder="Siparişe genel açıklama giriniz (örn. teslimat, üretim notu, müşteri isteği...)"
|
|
||||||
:disable="isClosedRow"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div> <!-- editor -->
|
|
||||||
</div> <!-- ✅ order-scroll-y -->
|
</div> <!-- ✅ order-scroll-y -->
|
||||||
|
</template>
|
||||||
|
|
||||||
</q-page>
|
</q-page>
|
||||||
<q-page
|
<q-page
|
||||||
@@ -777,6 +800,8 @@ const canExportOrder = canExport('order')
|
|||||||
const formatDate = formatDateDisplay
|
const formatDate = formatDateDisplay
|
||||||
|
|
||||||
|
|
||||||
|
const showEditor = ref(false)
|
||||||
|
const compactGridHeader = ref(false)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1094,6 +1119,16 @@ const seriMultiplier = ref(1)
|
|||||||
const loadingHeader = ref(true)
|
const loadingHeader = ref(true)
|
||||||
const loadingCari = ref(true)
|
const loadingCari = ref(true)
|
||||||
const loadingModels = ref(true)
|
const loadingModels = ref(true)
|
||||||
|
const isPageBlocking = computed(() => {
|
||||||
|
if (!isEditMode.value) return false
|
||||||
|
const headerReady = !!orderStore.header
|
||||||
|
return (
|
||||||
|
loadingHeader.value ||
|
||||||
|
loadingCari.value ||
|
||||||
|
loadingModels.value ||
|
||||||
|
!headerReady
|
||||||
|
)
|
||||||
|
})
|
||||||
/* ===========================================================
|
/* ===========================================================
|
||||||
🔹 CARİ INFO STATE
|
🔹 CARİ INFO STATE
|
||||||
=========================================================== */
|
=========================================================== */
|
||||||
@@ -2346,11 +2381,22 @@ const editRow = async (row) => {
|
|||||||
notify: true,
|
notify: true,
|
||||||
loadSizes: true
|
loadSizes: true
|
||||||
})
|
})
|
||||||
|
showEditor.value = true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('❌ editRow hata:', err)
|
console.error('❌ editRow hata:', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openNewRowEditor = async () => {
|
||||||
|
if (!hasRowMutationPermission()) {
|
||||||
|
notifyNoPermission('Siparis satiri ekleme/guncelleme yetkiniz yok')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await resetEditor(true)
|
||||||
|
showEditor.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -2765,6 +2811,7 @@ const onSaveOrUpdateRow = async () => {
|
|||||||
stockMap,
|
stockMap,
|
||||||
$q
|
$q
|
||||||
})
|
})
|
||||||
|
showEditor.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,13 @@
|
|||||||
</q-icon>
|
</q-icon>
|
||||||
</q-td>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
|
<template #body-cell-HasUretimUrunu="props">
|
||||||
|
<q-td :props="props" class="text-left">
|
||||||
|
<span v-if="props.row.HasUretimUrunu" class="text-weight-bold text-negative">
|
||||||
|
ÜRETİME VERİLECEK ÜRÜNÜ VAR
|
||||||
|
</span>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #body-cell-OrderDate="props">
|
<template #body-cell-OrderDate="props">
|
||||||
<q-td :props="props" class="text-center">
|
<q-td :props="props" class="text-center">
|
||||||
@@ -366,6 +373,7 @@ const columns = [
|
|||||||
format: val => Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' %'
|
format: val => Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' %'
|
||||||
},
|
},
|
||||||
{ name: 'IsCreditableConfirmed', label: 'Durum', field: 'IsCreditableConfirmed', align: 'center', sortable: true },
|
{ name: 'IsCreditableConfirmed', label: 'Durum', field: 'IsCreditableConfirmed', align: 'center', sortable: true },
|
||||||
|
{ name: 'HasUretimUrunu', label: 'Üretim', field: 'HasUretimUrunu', align: 'left', sortable: true, style: 'min-width:190px;white-space:nowrap', headerStyle: 'min-width:190px;white-space:nowrap' },
|
||||||
{ name: 'Description', label: 'Açıklama', field: 'Description', align: 'left', sortable: false, classes: 'ol-col-desc', headerClasses: 'ol-col-desc', style: 'width:160px;max-width:160px', headerStyle: 'width:160px;max-width:160px' },
|
{ name: 'Description', label: 'Açıklama', field: 'Description', align: 'left', sortable: false, classes: 'ol-col-desc', headerClasses: 'ol-col-desc', style: 'width:160px;max-width:160px', headerStyle: 'width:160px;max-width:160px' },
|
||||||
{ name: 'pdf', label: 'PDF', field: 'pdf', align: 'center', sortable: false }
|
{ name: 'pdf', label: 'PDF', field: 'pdf', align: 'center', sortable: false }
|
||||||
]
|
]
|
||||||
|
|||||||
372
ui/src/pages/OrderProductionUpdate.vue
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
<template>
|
||||||
|
<q-page class="q-pa-md">
|
||||||
|
<div class="row items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-h6 text-weight-bold">Uretime Verilen Urunleri Guncelle</div>
|
||||||
|
<div class="text-caption text-grey-7 q-mt-xs">
|
||||||
|
OrderHeaderID: {{ orderHeaderID || '-' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
icon="refresh"
|
||||||
|
label="Yenile"
|
||||||
|
:loading="store.loading"
|
||||||
|
@click="refreshAll"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-bar row q-col-gutter-md q-mt-md">
|
||||||
|
<div class="col-5">
|
||||||
|
<q-input
|
||||||
|
:model-value="cariLabel"
|
||||||
|
label="Cari Secimi"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<q-input
|
||||||
|
:model-value="header?.OrderNumber || ''"
|
||||||
|
label="Siparis No"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<q-input
|
||||||
|
:model-value="formatDate(header?.OrderDate)"
|
||||||
|
label="Olusturulma Tarihi"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<q-input
|
||||||
|
:model-value="formatDate(header?.AverageDueDate)"
|
||||||
|
label="Tahmini Termin Tarihi"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-table
|
||||||
|
class="q-mt-md"
|
||||||
|
flat
|
||||||
|
bordered
|
||||||
|
dense
|
||||||
|
separator="cell"
|
||||||
|
row-key="OrderLineID"
|
||||||
|
:rows="rows"
|
||||||
|
:columns="columns"
|
||||||
|
:loading="store.loading"
|
||||||
|
no-data-label="Uretime verilecek urun bulunamadi"
|
||||||
|
:rows-per-page-options="[0]"
|
||||||
|
hide-bottom
|
||||||
|
>
|
||||||
|
<template #body-cell-actions="props">
|
||||||
|
<q-td :props="props" class="text-center">
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
icon="save"
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
:loading="rowSavingId === props.row.OrderLineID"
|
||||||
|
@click="onRowSubmit(props.row)"
|
||||||
|
>
|
||||||
|
<q-tooltip>Satiri Guncelle</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell-NewItemCode="props">
|
||||||
|
<q-td :props="props">
|
||||||
|
<q-input
|
||||||
|
v-model="props.row.NewItemCode"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
label="Yeni Urun"
|
||||||
|
@update:model-value="val => onNewItemChange(props.row, val)"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<q-icon name="arrow_drop_down" class="cursor-pointer" />
|
||||||
|
</template>
|
||||||
|
<q-menu
|
||||||
|
anchor="bottom left"
|
||||||
|
self="top left"
|
||||||
|
fit
|
||||||
|
>
|
||||||
|
<div class="q-pa-sm" style="min-width:260px">
|
||||||
|
<q-input
|
||||||
|
v-model="productSearch"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
debounce="200"
|
||||||
|
placeholder="Urun ara..."
|
||||||
|
/>
|
||||||
|
<q-list class="q-mt-xs" bordered separator>
|
||||||
|
<q-item
|
||||||
|
v-for="opt in filteredProducts"
|
||||||
|
:key="opt.ProductCode"
|
||||||
|
clickable
|
||||||
|
@click="onSelectProduct(props.row, opt.ProductCode)"
|
||||||
|
>
|
||||||
|
<q-item-section>{{ opt.ProductCode }}</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</div>
|
||||||
|
</q-menu>
|
||||||
|
</q-input>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell-NewColor="props">
|
||||||
|
<q-td :props="props">
|
||||||
|
<q-select
|
||||||
|
v-model="props.row.NewColor"
|
||||||
|
:options="getColorOptions(props.row)"
|
||||||
|
option-label="colorLabel"
|
||||||
|
option-value="color_code"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
use-input
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
label="Yeni Renk"
|
||||||
|
@update:model-value="() => onNewColorChange(props.row)"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell-NewDim2="props">
|
||||||
|
<q-td :props="props">
|
||||||
|
<q-select
|
||||||
|
v-model="props.row.NewDim2"
|
||||||
|
:options="getSecondColorOptions(props.row)"
|
||||||
|
option-label="item_dim2_code"
|
||||||
|
option-value="item_dim2_code"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
use-input
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
label="Yeni 2. Renk"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell-NewDesc="props">
|
||||||
|
<q-td :props="props">
|
||||||
|
<q-input
|
||||||
|
v-model="props.row.NewDesc"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
label="Yeni Aciklama"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
|
||||||
|
<q-banner v-if="store.error" class="bg-red text-white q-mt-sm">
|
||||||
|
Hata: {{ store.error }}
|
||||||
|
</q-banner>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useQuasar } from 'quasar'
|
||||||
|
import { useOrderProductionItemStore } from 'src/stores/OrderProductionItemStore'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const $q = useQuasar()
|
||||||
|
const store = useOrderProductionItemStore()
|
||||||
|
|
||||||
|
const orderHeaderID = computed(() => String(route.params.orderHeaderID || '').trim())
|
||||||
|
const header = computed(() => store.header || {})
|
||||||
|
const cariLabel = computed(() => {
|
||||||
|
const code = header.value?.CurrAccCode || ''
|
||||||
|
const name = header.value?.CurrAccDescription || ''
|
||||||
|
if (!code && !name) return ''
|
||||||
|
if (!name) return code
|
||||||
|
return `${code} - ${name}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const rows = ref([])
|
||||||
|
const productOptions = ref([])
|
||||||
|
const productSearch = ref('')
|
||||||
|
const rowSavingId = ref('')
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ name: 'OldItemCode', label: 'Eski Urun Kodu', field: 'OldItemCode', align: 'left', sortable: true, style: 'min-width:140px;white-space:nowrap', headerStyle: 'min-width:140px;white-space:nowrap' },
|
||||||
|
{ name: 'OldColor', label: 'Eski Urun Rengi', field: 'OldColor', align: 'left', sortable: true, style: 'min-width:120px;white-space:nowrap', headerStyle: 'min-width:120px;white-space:nowrap' },
|
||||||
|
{ name: 'OldDim2', label: 'Eski 2. Renk', field: 'OldDim2', align: 'left', sortable: true, style: 'min-width:110px;white-space:nowrap', headerStyle: 'min-width:110px;white-space:nowrap' },
|
||||||
|
{ name: 'OldDesc', label: 'Eski Aciklama', field: 'OldDesc', align: 'left', sortable: false, style: 'min-width:180px;white-space:nowrap', headerStyle: 'min-width:180px;white-space:nowrap' },
|
||||||
|
{ name: 'NewItemCode', label: 'Yeni Urun Kodu', field: 'NewItemCode', align: 'left', sortable: false, style: 'min-width:190px;', headerStyle: 'min-width:190px;' },
|
||||||
|
{ name: 'NewColor', label: 'Yeni Urun Rengi', field: 'NewColor', align: 'left', sortable: false, style: 'min-width:160px;', headerStyle: 'min-width:160px;' },
|
||||||
|
{ name: 'NewDim2', label: 'Yeni 2. Renk', field: 'NewDim2', align: 'left', sortable: false, style: 'min-width:160px;', headerStyle: 'min-width:160px;' },
|
||||||
|
{ name: 'NewDesc', label: 'Yeni Aciklama', field: 'NewDesc', align: 'left', sortable: false, style: 'min-width:220px;', headerStyle: 'min-width:220px;' },
|
||||||
|
{ name: 'actions', label: '', field: 'actions', align: 'center', sortable: false, style: 'width:60px;', headerStyle: 'width:60px;' }
|
||||||
|
]
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await refreshAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(orderHeaderID, async (id) => {
|
||||||
|
await refreshAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => store.items,
|
||||||
|
(items) => {
|
||||||
|
rows.value = (items || []).map(item => ({
|
||||||
|
...item,
|
||||||
|
NewItemCode: '',
|
||||||
|
NewColor: '',
|
||||||
|
NewDim2: '',
|
||||||
|
NewDesc: ''
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => store.products,
|
||||||
|
(products) => {
|
||||||
|
productOptions.value = products || []
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
function formatDate (val) {
|
||||||
|
if (!val) return ''
|
||||||
|
const text = String(val)
|
||||||
|
return text.length >= 10 ? text.slice(0, 10) : text
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredProducts = computed(() => {
|
||||||
|
const needle = String(productSearch.value || '').toLowerCase()
|
||||||
|
if (!needle) return productOptions.value.slice(0, 50)
|
||||||
|
return productOptions.value.filter(p =>
|
||||||
|
String(p?.ProductCode || '').toLowerCase().includes(needle)
|
||||||
|
).slice(0, 50)
|
||||||
|
})
|
||||||
|
|
||||||
|
function onSelectProduct (row, code) {
|
||||||
|
productSearch.value = ''
|
||||||
|
onNewItemChange(row, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNewItemChange (row, val) {
|
||||||
|
const next = String(val || '').trim()
|
||||||
|
if (next && !isValidModelCode(next)) {
|
||||||
|
$q.notify({ type: 'negative', message: 'Model kodu formati gecersiz. Ornek: S000-DMY00001' })
|
||||||
|
row.NewItemCode = ''
|
||||||
|
row.NewColor = ''
|
||||||
|
row.NewDim2 = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
row.NewItemCode = next ? next.toUpperCase() : ''
|
||||||
|
row.NewColor = ''
|
||||||
|
row.NewDim2 = ''
|
||||||
|
if (row.NewItemCode) {
|
||||||
|
store.fetchColors(row.NewItemCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNewColorChange (row) {
|
||||||
|
row.NewDim2 = ''
|
||||||
|
if (row.NewItemCode && row.NewColor) {
|
||||||
|
store.fetchSecondColors(row.NewItemCode, row.NewColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColorOptions (row) {
|
||||||
|
const code = row?.NewItemCode || ''
|
||||||
|
const list = store.colorOptionsByCode[code] || []
|
||||||
|
return list.map(c => ({
|
||||||
|
...c,
|
||||||
|
colorLabel: `${c.color_code} - ${c.color_description || ''}`.trim()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSecondColorOptions (row) {
|
||||||
|
const code = row?.NewItemCode || ''
|
||||||
|
const color = row?.NewColor || ''
|
||||||
|
const key = `${code}::${color}`
|
||||||
|
return store.secondColorOptionsByKey[key] || []
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidModelCode (value) {
|
||||||
|
const text = String(value || '').trim().toUpperCase()
|
||||||
|
return /^[A-Z][0-9]{3}-[A-Z]{3}[0-9]{5}$/.test(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPayloadLines () {
|
||||||
|
return rows.value.map(r => ({
|
||||||
|
OrderLineID: r.OrderLineID,
|
||||||
|
NewItemCode: String(r.NewItemCode || '').trim(),
|
||||||
|
NewColor: String(r.NewColor || '').trim(),
|
||||||
|
NewDim2: String(r.NewDim2 || '').trim(),
|
||||||
|
NewDesc: String(r.NewDesc || '').trim()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshAll () {
|
||||||
|
await store.fetchHeader(orderHeaderID.value)
|
||||||
|
await store.fetchItems(orderHeaderID.value)
|
||||||
|
await store.fetchProducts()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRowSubmit (row) {
|
||||||
|
const line = {
|
||||||
|
OrderLineID: row.OrderLineID,
|
||||||
|
NewItemCode: String(row.NewItemCode || '').trim(),
|
||||||
|
NewColor: String(row.NewColor || '').trim(),
|
||||||
|
NewDim2: String(row.NewDim2 || '').trim(),
|
||||||
|
NewDesc: String(row.NewDesc || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!line.NewItemCode || !line.NewColor) {
|
||||||
|
$q.notify({ type: 'negative', message: 'Yeni urun ve renk zorunludur.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rowSavingId.value = row.OrderLineID
|
||||||
|
try {
|
||||||
|
const validate = await store.validateUpdates(orderHeaderID.value, [line])
|
||||||
|
const missingCount = validate?.missingCount || 0
|
||||||
|
if (missingCount > 0) {
|
||||||
|
const missingList = (validate?.missing || []).map(v => (
|
||||||
|
`${v.ItemCode} / ${v.ColorCode} / ${v.ItemDim1Code} / ${v.ItemDim2Code}`
|
||||||
|
))
|
||||||
|
$q.dialog({
|
||||||
|
title: 'Eksik Varyantlar',
|
||||||
|
message: `Eksik varyant bulundu: ${missingCount}<br><br>${missingList.join('<br>')}`,
|
||||||
|
html: true,
|
||||||
|
ok: { label: 'Ekle ve Guncelle', color: 'primary' },
|
||||||
|
cancel: { label: 'Vazgec', flat: true }
|
||||||
|
}).onOk(async () => {
|
||||||
|
await store.applyUpdates(orderHeaderID.value, [line], true)
|
||||||
|
await store.fetchItems(orderHeaderID.value)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await store.applyUpdates(orderHeaderID.value, [line], false)
|
||||||
|
await store.fetchItems(orderHeaderID.value)
|
||||||
|
} catch (err) {
|
||||||
|
$q.notify({ type: 'negative', message: 'Islem basarisiz.' })
|
||||||
|
} finally {
|
||||||
|
rowSavingId.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
357
ui/src/pages/OrderProductionUpdateList.vue
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
<template>
|
||||||
|
<q-page v-if="canReadOrder" class="ol-page">
|
||||||
|
<div class="ol-filter-bar">
|
||||||
|
<div class="ol-filter-row">
|
||||||
|
<q-input
|
||||||
|
v-model="store.filters.search"
|
||||||
|
class="ol-filter-input ol-search"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
debounce="300"
|
||||||
|
clearable
|
||||||
|
label="Arama (Sipariş No / Cari / Açıklama)"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<q-icon name="search" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
v-model="store.filters.CurrAccCode"
|
||||||
|
class="ol-filter-input"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
clearable
|
||||||
|
label="Cari Kodu"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
v-model="store.filters.OrderDate"
|
||||||
|
class="ol-filter-input"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
type="date"
|
||||||
|
label="Sipariş Tarihi"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="ol-filter-actions">
|
||||||
|
<q-btn
|
||||||
|
label="Temizle"
|
||||||
|
icon="clear"
|
||||||
|
color="grey-7"
|
||||||
|
flat
|
||||||
|
:disable="store.loading"
|
||||||
|
@click="clearFilters"
|
||||||
|
>
|
||||||
|
<q-tooltip>Tüm filtreleri temizle</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
label="Yenile"
|
||||||
|
color="primary"
|
||||||
|
icon="refresh"
|
||||||
|
:loading="store.loading"
|
||||||
|
@click="store.fetchOrders"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
label="Excel'e Aktar"
|
||||||
|
icon="download"
|
||||||
|
color="primary"
|
||||||
|
outline
|
||||||
|
:disable="store.loading || productionOrders.length === 0"
|
||||||
|
@click="exportExcel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ol-filter-total">
|
||||||
|
<div class="ol-total-line">
|
||||||
|
<span class="ol-total-label">Toplam USD:</span>
|
||||||
|
<strong class="ol-total-value">
|
||||||
|
{{ totalVisibleUSD.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-table
|
||||||
|
title="Üretime Verilecek Ürünleri Olan Siparişler"
|
||||||
|
class="ol-table"
|
||||||
|
flat
|
||||||
|
bordered
|
||||||
|
dense
|
||||||
|
separator="cell"
|
||||||
|
row-key="OrderHeaderID"
|
||||||
|
:rows="productionOrders"
|
||||||
|
:columns="columns"
|
||||||
|
:loading="store.loading"
|
||||||
|
no-data-label="Üretime verilecek sipariş bulunamadı"
|
||||||
|
:rows-per-page-options="[0]"
|
||||||
|
hide-bottom
|
||||||
|
>
|
||||||
|
<template #body-cell-open="props">
|
||||||
|
<q-td :props="props" class="text-center">
|
||||||
|
<q-btn
|
||||||
|
icon="open_in_new"
|
||||||
|
color="primary"
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
@click="selectOrder(props.row)"
|
||||||
|
>
|
||||||
|
<q-tooltip>Siparişi Aç</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell-IsCreditableConfirmed="props">
|
||||||
|
<q-td :props="props" class="text-center q-gutter-sm">
|
||||||
|
<q-btn
|
||||||
|
icon="picture_as_pdf"
|
||||||
|
color="red"
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
@click="printPDF(props.row)"
|
||||||
|
>
|
||||||
|
<q-tooltip>Siparişi PDF olarak aç</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<q-icon
|
||||||
|
:name="props.row.IsCreditableConfirmed ? 'check_circle' : 'cancel'"
|
||||||
|
:color="props.row.IsCreditableConfirmed ? 'green' : 'red'"
|
||||||
|
size="20px"
|
||||||
|
>
|
||||||
|
<q-tooltip>
|
||||||
|
{{ props.row.IsCreditableConfirmed ? 'Onaylı' : 'Onaysız' }}
|
||||||
|
</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell-OrderDate="props">
|
||||||
|
<q-td :props="props" class="text-center">
|
||||||
|
{{ formatDate(props.row.OrderDate) }}
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell-CreditableConfirmedDate="props">
|
||||||
|
<q-td :props="props" class="text-center">
|
||||||
|
{{ formatDate(props.row.CreditableConfirmedDate) }}
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<template #body-cell-CurrAccDescription="props">
|
||||||
|
<q-td :props="props" class="ol-col-cari">
|
||||||
|
<div class="ol-col-multiline">{{ props.value }}</div>
|
||||||
|
<q-tooltip v-if="props.value">
|
||||||
|
{{ props.value }}
|
||||||
|
</q-tooltip>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell-MusteriTemsilcisi="props">
|
||||||
|
<q-td :props="props" class="ol-col-short">
|
||||||
|
<div class="ol-col-multiline">{{ props.value }}</div>
|
||||||
|
<q-tooltip v-if="props.value">
|
||||||
|
{{ props.value }}
|
||||||
|
</q-tooltip>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell-Piyasa="props">
|
||||||
|
<q-td :props="props" class="ol-col-short">
|
||||||
|
<div class="ol-col-multiline">{{ props.value }}</div>
|
||||||
|
<q-tooltip v-if="props.value">
|
||||||
|
{{ props.value }}
|
||||||
|
</q-tooltip>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell-Description="props">
|
||||||
|
<q-td :props="props" class="ol-col-desc">
|
||||||
|
<div class="ol-col-multiline">{{ props.value }}</div>
|
||||||
|
<q-tooltip v-if="props.value">
|
||||||
|
{{ props.value }}
|
||||||
|
</q-tooltip>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell-HasUretimUrunu="props">
|
||||||
|
<q-td :props="props" class="text-center">
|
||||||
|
<q-icon
|
||||||
|
:name="props.row.HasUretimUrunu ? 'check_circle' : 'cancel'"
|
||||||
|
:color="props.row.HasUretimUrunu ? 'green' : 'grey-5'"
|
||||||
|
size="18px"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
|
||||||
|
<q-banner v-if="store.error" class="bg-red text-white q-mt-sm">
|
||||||
|
Hata: {{ store.error }}
|
||||||
|
</q-banner>
|
||||||
|
</q-page>
|
||||||
|
|
||||||
|
<q-page v-else class="q-pa-md flex flex-center">
|
||||||
|
<div class="text-negative text-subtitle1">
|
||||||
|
Bu modüle erişim yetkiniz yok.
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, watch } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useQuasar } from 'quasar'
|
||||||
|
import { useOrderProductionUpdateStore } from 'src/stores/OrderProductionUpdateStore'
|
||||||
|
import { useAuthStore } from 'src/stores/authStore'
|
||||||
|
import { usePermission } from 'src/composables/usePermission'
|
||||||
|
import api, { extractApiErrorDetail } from 'src/services/api'
|
||||||
|
|
||||||
|
const { canRead } = usePermission()
|
||||||
|
const canReadOrder = canRead('order')
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const $q = useQuasar()
|
||||||
|
const store = useOrderProductionUpdateStore()
|
||||||
|
|
||||||
|
let searchTimer = null
|
||||||
|
watch(
|
||||||
|
() => store.filters.search,
|
||||||
|
() => {
|
||||||
|
clearTimeout(searchTimer)
|
||||||
|
searchTimer = setTimeout(() => {
|
||||||
|
store.fetchOrders()
|
||||||
|
}, 400)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const productionOrders = computed(() => store.filteredOrders)
|
||||||
|
|
||||||
|
const totalVisibleUSD = computed(() =>
|
||||||
|
productionOrders.value.reduce((sum, o) => {
|
||||||
|
const v = Number(o.TotalAmountUSD || 0)
|
||||||
|
return sum + (Number.isFinite(v) ? v : 0)
|
||||||
|
}, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
function clearFilters () {
|
||||||
|
store.filters.search = ''
|
||||||
|
store.filters.CurrAccCode = ''
|
||||||
|
store.filters.OrderDate = ''
|
||||||
|
store.fetchOrders()
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate (v) {
|
||||||
|
if (!v) return ''
|
||||||
|
return String(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectOrder (row) {
|
||||||
|
if (!row?.OrderHeaderID) {
|
||||||
|
$q.notify({ type: 'warning', message: 'OrderHeaderID bulunamadı' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
name: 'orderproductionupdate',
|
||||||
|
params: { orderHeaderID: row.OrderHeaderID }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function printPDF (row) {
|
||||||
|
try {
|
||||||
|
const auth = useAuthStore()
|
||||||
|
if (!auth?.token) {
|
||||||
|
$q.notify({ type: 'warning', message: 'Oturum bulunamadı' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await api.get(`/order/pdf/${row.OrderHeaderID}`, {
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
|
||||||
|
const blob = new Blob([res.data], { type: 'application/pdf' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
window.open(url, '_blank')
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 2000)
|
||||||
|
} catch (err) {
|
||||||
|
const status = err?.response?.status
|
||||||
|
const detail = extractApiErrorDetail(err)
|
||||||
|
console.error(`PDF load error [${status}] /order/pdf/${row?.OrderHeaderID}: ${detail}`)
|
||||||
|
$q.notify({ type: 'negative', message: `PDF alınamadı: ${detail}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportExcel () {
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
if (!auth?.token) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: 'Oturum bulunamadı'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (store.filters.search) params.append('search', store.filters.search)
|
||||||
|
|
||||||
|
api.get(`/orders/production-list?${params.toString()}`, {
|
||||||
|
responseType: 'blob'
|
||||||
|
}).then((res) => {
|
||||||
|
const blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = 'uretime_verilecek_siparisler.xlsx'
|
||||||
|
a.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}).catch((err) => {
|
||||||
|
const detail = extractApiErrorDetail(err)
|
||||||
|
$q.notify({ type: 'negative', message: `Excel alınamadı: ${detail}` })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
store.fetchOrders()
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ name: 'open', label: '', field: 'open', align: 'center', sortable: false },
|
||||||
|
{ name: 'OrderNumber', label: 'Sipariş No', field: 'OrderNumber', align: 'left', sortable: true, style: 'min-width:108px;white-space:nowrap', headerStyle: 'min-width:108px;white-space:nowrap' },
|
||||||
|
{ name: 'OrderDate', label: 'Tarih', field: 'OrderDate', align: 'center', sortable: true, style: 'min-width:82px;white-space:nowrap', headerStyle: 'min-width:82px;white-space:nowrap' },
|
||||||
|
{ name: 'CurrAccCode', label: 'Cari Kod', field: 'CurrAccCode', align: 'left', sortable: true, style: 'min-width:82px;white-space:nowrap', headerStyle: 'min-width:82px;white-space:nowrap' },
|
||||||
|
{ name: 'CurrAccDescription', label: 'Cari Adı', field: 'CurrAccDescription', align: 'left', sortable: true, classes: 'ol-col-cari', headerClasses: 'ol-col-cari', style: 'width:160px;max-width:160px', headerStyle: 'width:160px;max-width:160px' },
|
||||||
|
{ name: 'MusteriTemsilcisi', label: 'Temsilci', field: 'MusteriTemsilcisi', align: 'left', sortable: true, classes: 'ol-col-short', headerClasses: 'ol-col-short', style: 'width:88px;max-width:88px', headerStyle: 'width:88px;max-width:88px' },
|
||||||
|
{ name: 'Piyasa', label: 'Piyasa', field: 'Piyasa', align: 'left', sortable: true, classes: 'ol-col-short', headerClasses: 'ol-col-short', style: 'width:72px;max-width:72px', headerStyle: 'width:72px;max-width:72px' },
|
||||||
|
{ name: 'CreditableConfirmedDate', label: 'Onay', field: 'CreditableConfirmedDate', align: 'center', sortable: true, style: 'min-width:86px;white-space:nowrap', headerStyle: 'min-width:86px;white-space:nowrap' },
|
||||||
|
{ name: 'DocCurrencyCode', label: 'PB', field: 'DocCurrencyCode', align: 'center', sortable: true, style: 'min-width:46px;white-space:nowrap', headerStyle: 'min-width:46px;white-space:nowrap' },
|
||||||
|
{
|
||||||
|
name: 'TotalAmount',
|
||||||
|
label: 'Tutar',
|
||||||
|
field: 'TotalAmount',
|
||||||
|
align: 'right',
|
||||||
|
sortable: true,
|
||||||
|
style: 'min-width:120px;white-space:nowrap',
|
||||||
|
headerStyle: 'min-width:120px;white-space:nowrap',
|
||||||
|
format: (val, row) => Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' ' + row.DocCurrencyCode
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TotalAmountUSD',
|
||||||
|
label: 'Tutar (USD)',
|
||||||
|
field: 'TotalAmountUSD',
|
||||||
|
align: 'right',
|
||||||
|
sortable: true,
|
||||||
|
style: 'min-width:120px;white-space:nowrap',
|
||||||
|
headerStyle: 'min-width:120px;white-space:nowrap',
|
||||||
|
format: val => Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' USD'
|
||||||
|
},
|
||||||
|
{ name: 'IsCreditableConfirmed', label: 'Durum', field: 'IsCreditableConfirmed', align: 'center', sortable: true },
|
||||||
|
{ name: 'HasUretimUrunu', label: 'Üretim', field: 'HasUretimUrunu', align: 'left', sortable: true, style: 'min-width:190px;white-space:nowrap', headerStyle: 'min-width:190px;white-space:nowrap' },
|
||||||
|
{ name: 'Description', label: 'Açıklama', field: 'Description', align: 'left', sortable: false, classes: 'ol-col-desc', headerClasses: 'ol-col-desc', style: 'width:160px;max-width:160px', headerStyle: 'width:160px;max-width:160px' }
|
||||||
|
]
|
||||||
|
</script>
|
||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -224,6 +224,15 @@
|
|||||||
filled
|
filled
|
||||||
behavior="menu"
|
behavior="menu"
|
||||||
>
|
>
|
||||||
|
<template #before-options>
|
||||||
|
<q-item clickable @click="selectAllPiyasalar">
|
||||||
|
<q-item-section>Tümünü Seç</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable @click="clearPiyasalar">
|
||||||
|
<q-item-section>Temizle</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-separator />
|
||||||
|
</template>
|
||||||
<template #selected-item="scope">
|
<template #selected-item="scope">
|
||||||
<q-chip
|
<q-chip
|
||||||
removable
|
removable
|
||||||
@@ -240,6 +249,8 @@
|
|||||||
<q-checkbox
|
<q-checkbox
|
||||||
:model-value="scope.selected"
|
:model-value="scope.selected"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
|
@update:model-value="() => scope.toggleOption(scope.opt)"
|
||||||
|
@click.stop
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
@@ -352,6 +363,16 @@ const canSendPasswordMail = computed(() => {
|
|||||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((form.value.email || '').trim())
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((form.value.email || '').trim())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function selectAllPiyasalar () {
|
||||||
|
form.value.piyasalar = (piyasaOptions.value || [])
|
||||||
|
.map((o) => o.value)
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPiyasalar () {
|
||||||
|
form.value.piyasalar = []
|
||||||
|
}
|
||||||
|
|
||||||
/* ================= LIFECYCLE ================= */
|
/* ================= LIFECYCLE ================= */
|
||||||
watch(
|
watch(
|
||||||
() => userId.value,
|
() => userId.value,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-page v-if="canReadFinance" class="q-pa-md page-col">
|
<q-page v-if="canReadFinance" class="q-pa-md page-col statement-page">
|
||||||
|
|
||||||
<!-- 🔹 Cari Kod / İsim (sabit) -->
|
<!-- 🔹 Cari Kod / İsim (sabit) -->
|
||||||
<div class="filter-sticky">
|
<div class="filter-sticky">
|
||||||
@@ -47,7 +47,12 @@
|
|||||||
<template #append>
|
<template #append>
|
||||||
<q-icon name="event" class="cursor-pointer">
|
<q-icon name="event" class="cursor-pointer">
|
||||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
||||||
<q-date v-model="dateFrom" mask="YYYY-MM-DD" locale="tr-TR"/>
|
<q-date
|
||||||
|
v-model="dateFrom"
|
||||||
|
mask="YYYY-MM-DD"
|
||||||
|
locale="tr-TR"
|
||||||
|
:options="isValidFromDate"
|
||||||
|
/>
|
||||||
</q-popup-proxy>
|
</q-popup-proxy>
|
||||||
</q-icon>
|
</q-icon>
|
||||||
</template>
|
</template>
|
||||||
@@ -63,7 +68,12 @@
|
|||||||
<template #append>
|
<template #append>
|
||||||
<q-icon name="event" class="cursor-pointer">
|
<q-icon name="event" class="cursor-pointer">
|
||||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
||||||
<q-date v-model="dateTo" mask="YYYY-MM-DD" locale="tr-TR" />
|
<q-date
|
||||||
|
v-model="dateTo"
|
||||||
|
mask="YYYY-MM-DD"
|
||||||
|
locale="tr-TR"
|
||||||
|
:options="isValidToDate"
|
||||||
|
/>
|
||||||
</q-popup-proxy>
|
</q-popup-proxy>
|
||||||
</q-icon>
|
</q-icon>
|
||||||
</template>
|
</template>
|
||||||
@@ -163,19 +173,21 @@
|
|||||||
|
|
||||||
<!-- Ana Tablo -->
|
<!-- Ana Tablo -->
|
||||||
<q-table
|
<q-table
|
||||||
class="sticky-table"
|
class="sticky-table statement-table"
|
||||||
title="Hareketler"
|
title="Hareketler"
|
||||||
:rows="statementheaderStore.groupedRows"
|
:rows="statementheaderStore.groupedRows"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:visible-columns="visibleColumns"
|
:visible-columns="visibleColumns"
|
||||||
:row-key="row => row.OrderHeaderID + '_' + row.OrderNumber"
|
:row-key="rowKeyFn"
|
||||||
|
|
||||||
flat
|
flat
|
||||||
bordered
|
bordered
|
||||||
dense
|
dense
|
||||||
|
hide-bottom
|
||||||
|
wrap-cells
|
||||||
:rows-per-page-options="[0]"
|
:rows-per-page-options="[0]"
|
||||||
:loading="statementheaderStore.loading"
|
:loading="statementheaderStore.loading"
|
||||||
:table-style="{ tableLayout: 'auto', minWidth: '1600px' }"
|
:table-style="{ tableLayout: 'fixed', width: '100%' }"
|
||||||
>
|
>
|
||||||
<template #body="props">
|
<template #body="props">
|
||||||
|
|
||||||
@@ -332,6 +344,29 @@ onMounted(async () => {
|
|||||||
const dateFrom = ref(dayjs().startOf('year').format('YYYY-MM-DD'))
|
const dateFrom = ref(dayjs().startOf('year').format('YYYY-MM-DD'))
|
||||||
const dateTo = ref(dayjs().format('YYYY-MM-DD'))
|
const dateTo = ref(dayjs().format('YYYY-MM-DD'))
|
||||||
|
|
||||||
|
function isValidFromDate (date) {
|
||||||
|
if (!dateTo.value) return true
|
||||||
|
return !dayjs(date).isAfter(dayjs(dateTo.value), 'day')
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidToDate (date) {
|
||||||
|
if (!dateFrom.value) return true
|
||||||
|
return !dayjs(date).isBefore(dayjs(dateFrom.value), 'day')
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasInvalidDateRange () {
|
||||||
|
if (!dateFrom.value || !dateTo.value) return false
|
||||||
|
return dayjs(dateFrom.value).isAfter(dayjs(dateTo.value), 'day')
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyInvalidDateRange () {
|
||||||
|
$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: '⚠️ Başlangıç tarihi bitiş tarihinden sonra olamaz.',
|
||||||
|
position: 'top-right'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/* Parasal İşlem Tipi */
|
/* Parasal İşlem Tipi */
|
||||||
const monetaryTypeOptions = [
|
const monetaryTypeOptions = [
|
||||||
{ label: '1-2 hesap', value: ['1', '2'] },
|
{ label: '1-2 hesap', value: ['1', '2'] },
|
||||||
@@ -373,6 +408,11 @@ async function onFilterClick() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasInvalidDateRange()) {
|
||||||
|
notifyInvalidDateRange()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await statementheaderStore.loadStatements({
|
await statementheaderStore.loadStatements({
|
||||||
startdate: dateFrom.value,
|
startdate: dateFrom.value,
|
||||||
enddate: dateTo.value,
|
enddate: dateTo.value,
|
||||||
@@ -483,6 +523,11 @@ async function handleDownload() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasInvalidDateRange()) {
|
||||||
|
notifyInvalidDateRange()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// ✅ Seçilen parasal işlem tipini gönder
|
// ✅ Seçilen parasal işlem tipini gönder
|
||||||
const result = await downloadstpdfStore.downloadPDF(
|
const result = await downloadstpdfStore.downloadPDF(
|
||||||
selectedCari.value, // accountCode
|
selectedCari.value, // accountCode
|
||||||
@@ -524,6 +569,11 @@ async function CurrheadDownload() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasInvalidDateRange()) {
|
||||||
|
notifyInvalidDateRange()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// ✅ Yeni store fonksiyonu doğru şekilde çağrılıyor
|
// ✅ Yeni store fonksiyonu doğru şekilde çağrılıyor
|
||||||
const result = await downloadstHeadStore.handlestHeadDownload(
|
const result = await downloadstHeadStore.handlestHeadDownload(
|
||||||
selectedCari.value, // accountCode
|
selectedCari.value, // accountCode
|
||||||
@@ -542,3 +592,100 @@ async function CurrheadDownload() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.statement-page {
|
||||||
|
height: calc(100vh - 56px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky-bar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 20;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statement-table {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statement-table :deep(.q-table__container) {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statement-table :deep(.q-table__top) {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statement-table :deep(.q-table__middle) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto !important;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statement-table :deep(thead th) {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statement-table :deep(th),
|
||||||
|
.statement-table :deep(td) {
|
||||||
|
padding: 3px 6px !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
line-height: 1.2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statement-table :deep(td) {
|
||||||
|
white-space: nowrap !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
text-overflow: ellipsis !important;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statement-table :deep(td[data-col="aciklama"]),
|
||||||
|
.statement-table :deep(th[data-col="aciklama"]) {
|
||||||
|
max-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statement-table :deep(.resizable-cell-content) {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: normal !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1366px) {
|
||||||
|
.statement-table :deep(th),
|
||||||
|
.statement-table :deep(td) {
|
||||||
|
font-size: 10px !important;
|
||||||
|
padding: 2px 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statement-table :deep(td) {
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statement-table :deep(td[data-col="aciklama"]),
|
||||||
|
.statement-table :deep(th[data-col="aciklama"]) {
|
||||||
|
max-width: 180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ export default route(function () {
|
|||||||
routes
|
routes
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined' && process.env.DEV) {
|
||||||
|
window.__router = router
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
🔐 GLOBAL GUARD
|
🔐 GLOBAL GUARD
|
||||||
@@ -23,6 +26,17 @@ export default route(function () {
|
|||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const perm = usePermissionStore()
|
const perm = usePermissionStore()
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
console.warn('🧭 ROUTE GUARD HIT:', {
|
||||||
|
path: to.fullPath,
|
||||||
|
meta: to.meta
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined' && process.env.DEV) {
|
||||||
|
window.__auth = auth
|
||||||
|
window.__perm = perm
|
||||||
|
}
|
||||||
|
|
||||||
/* ================= PUBLIC ================= */
|
/* ================= PUBLIC ================= */
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const routes = [
|
|||||||
path: '',
|
path: '',
|
||||||
name: 'dashboard',
|
name: 'dashboard',
|
||||||
component: () => import('pages/Dashboard.vue'),
|
component: () => import('pages/Dashboard.vue'),
|
||||||
meta: { permission: 'system:read' }
|
meta: {}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
@@ -86,28 +86,28 @@ const routes = [
|
|||||||
path: 'role-dept-permissions',
|
path: 'role-dept-permissions',
|
||||||
name: 'role-dept-permissions',
|
name: 'role-dept-permissions',
|
||||||
component: () => import('pages/RoleDepartmentPermissionGateway.vue'),
|
component: () => import('pages/RoleDepartmentPermissionGateway.vue'),
|
||||||
meta: { permission: 'user:update' }
|
meta: { permission: 'system:update' }
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: 'role-dept-permissions/list',
|
path: 'role-dept-permissions/list',
|
||||||
name: 'role-dept-permissions-list',
|
name: 'role-dept-permissions-list',
|
||||||
component: () => import('pages/RoleDepartmentPermissionList.vue'),
|
component: () => import('pages/RoleDepartmentPermissionList.vue'),
|
||||||
meta: { permission: 'user:update' }
|
meta: { permission: 'system:update' }
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: 'role-dept-permissions/editor',
|
path: 'role-dept-permissions/editor',
|
||||||
name: 'role-dept-permissions-editor',
|
name: 'role-dept-permissions-editor',
|
||||||
component: () => import('pages/RoleDepartmentPermissionPage.vue'),
|
component: () => import('pages/RoleDepartmentPermissionPage.vue'),
|
||||||
meta: { permission: 'user:update' }
|
meta: { permission: 'system:update' }
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: 'user-permissions',
|
path: 'user-permissions',
|
||||||
name: 'user-permissions',
|
name: 'user-permissions',
|
||||||
component: () => import('pages/UserPermissionPage.vue'),
|
component: () => import('pages/UserPermissionPage.vue'),
|
||||||
meta: { permission: 'user:update' }
|
meta: { permission: 'system:update' }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ const routes = [
|
|||||||
path: 'activity-logs',
|
path: 'activity-logs',
|
||||||
name: 'activity-logs',
|
name: 'activity-logs',
|
||||||
component: () => import('pages/ActivityLogs.vue'),
|
component: () => import('pages/ActivityLogs.vue'),
|
||||||
meta: { permission: 'user:view' }
|
meta: { permission: 'system:read' }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ const routes = [
|
|||||||
path: 'test-mail',
|
path: 'test-mail',
|
||||||
name: 'test-mail',
|
name: 'test-mail',
|
||||||
component: () => import('pages/TestMail.vue'),
|
component: () => import('pages/TestMail.vue'),
|
||||||
meta: { permission: 'user:insert' }
|
meta: { permission: 'system:update' }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
@@ -242,6 +242,20 @@ const routes = [
|
|||||||
meta: { permission: 'order:view' }
|
meta: { permission: 'order:view' }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'orderproductionupdate',
|
||||||
|
name: 'orderproductionupdate-list',
|
||||||
|
component: () => import('pages/OrderProductionUpdateList.vue'),
|
||||||
|
meta: { permission: 'order:update' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'orderproductionupdate/:orderHeaderID',
|
||||||
|
name: 'orderproductionupdate',
|
||||||
|
component: () => import('pages/OrderProductionUpdate.vue'),
|
||||||
|
props: true,
|
||||||
|
meta: { permission: 'order:update' }
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: 'order-bulk-close',
|
path: 'order-bulk-close',
|
||||||
name: 'order-bulk-close',
|
name: 'order-bulk-close',
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import axios from 'axios'
|
|||||||
import qs from 'qs'
|
import qs from 'qs'
|
||||||
import { useAuthStore } from 'stores/authStore'
|
import { useAuthStore } from 'stores/authStore'
|
||||||
|
|
||||||
export const API_BASE_URL = '/api'
|
const rawBaseUrl =
|
||||||
|
(typeof process !== 'undefined' && process.env?.VITE_API_BASE_URL) || '/api'
|
||||||
|
|
||||||
|
export const API_BASE_URL = String(rawBaseUrl).trim().replace(/\/+$/, '')
|
||||||
|
const AUTH_REFRESH_PATH = '/auth/refresh'
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: API_BASE_URL,
|
baseURL: API_BASE_URL,
|
||||||
@@ -12,17 +16,69 @@ const api = axios.create({
|
|||||||
withCredentials: true
|
withCredentials: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function isPublicPath(url) {
|
||||||
|
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}`
|
||||||
}
|
}
|
||||||
@@ -31,33 +87,106 @@ api.interceptors.request.use((config) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
let isLoggingOut = false
|
let isLoggingOut = false
|
||||||
|
let refreshPromise = null
|
||||||
|
|
||||||
api.interceptors.response.use(
|
async function refreshAccessToken() {
|
||||||
r => r,
|
if (refreshPromise) {
|
||||||
async (error) => {
|
return refreshPromise
|
||||||
const status = error?.response?.status
|
|
||||||
const requestUrl = String(error?.config?.url || '')
|
|
||||||
const hasBlob = typeof Blob !== 'undefined' && error?.response?.data instanceof Blob
|
|
||||||
const isPasswordChangeRequest =
|
|
||||||
requestUrl.startsWith('/password/change') ||
|
|
||||||
requestUrl.startsWith('/me/password')
|
|
||||||
|
|
||||||
if ((status >= 500 || hasBlob) && error) {
|
|
||||||
const method = String(error?.config?.method || 'GET').toUpperCase()
|
|
||||||
const detail = await extractApiErrorDetail(error)
|
|
||||||
error.parsedMessage = detail
|
|
||||||
console.error(`API ${status || '-'} ${method} ${requestUrl}: ${detail}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Password change endpoints may return 401 for business reasons
|
refreshPromise = (async () => {
|
||||||
// (for example current password mismatch). Keep session in that case.
|
const auth = useAuthStore()
|
||||||
if (status === 401 && !isPasswordChangeRequest && !isLoggingOut) {
|
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
|
||||||
|
|
||||||
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)
|
||||||
@@ -114,6 +243,7 @@ async function parseBlobErrorMessage(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function extractApiErrorDetail(err) {
|
export async function extractApiErrorDetail(err) {
|
||||||
|
const status = err?.response?.status
|
||||||
let detail =
|
let detail =
|
||||||
err?.parsedMessage ||
|
err?.parsedMessage ||
|
||||||
err?.response?.data?.detail ||
|
err?.response?.data?.detail ||
|
||||||
@@ -129,7 +259,7 @@ export async function extractApiErrorDetail(err) {
|
|||||||
detail = err?.message || 'Request failed'
|
detail = err?.message || 'Request failed'
|
||||||
}
|
}
|
||||||
|
|
||||||
return detail
|
return sanitizeApiErrorDetail(detail, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const download = async (u, p = {}, c = {}) => {
|
export const download = async (u, p = {}, c = {}) => {
|
||||||
|
|||||||
147
ui/src/stores/OrderProductionItemStore.js
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
// src/stores/OrderProductionItemStore.js
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import api from 'src/services/api'
|
||||||
|
|
||||||
|
export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||||
|
state: () => ({
|
||||||
|
items: [],
|
||||||
|
header: null,
|
||||||
|
products: [],
|
||||||
|
colorOptionsByCode: {},
|
||||||
|
secondColorOptionsByKey: {},
|
||||||
|
loading: false,
|
||||||
|
saving: false,
|
||||||
|
error: null
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async fetchHeader (orderHeaderID) {
|
||||||
|
if (!orderHeaderID) {
|
||||||
|
this.header = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.get(`/order/get/${encodeURIComponent(orderHeaderID)}`)
|
||||||
|
this.header = res?.data?.header || null
|
||||||
|
} catch (err) {
|
||||||
|
this.header = null
|
||||||
|
this.error = err?.response?.data || err?.message || 'Siparis bilgisi alinamadi'
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async fetchItems (orderHeaderID) {
|
||||||
|
if (!orderHeaderID) {
|
||||||
|
this.items = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.get(`/orders/production-items/${encodeURIComponent(orderHeaderID)}`)
|
||||||
|
const data = res?.data
|
||||||
|
this.items = Array.isArray(data) ? data : []
|
||||||
|
} catch (err) {
|
||||||
|
this.items = []
|
||||||
|
this.error = err?.response?.data || err?.message || 'Liste alinamadi'
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async fetchProducts () {
|
||||||
|
this.error = null
|
||||||
|
try {
|
||||||
|
const res = await api.get('/products')
|
||||||
|
const data = res?.data
|
||||||
|
this.products = Array.isArray(data) ? data : []
|
||||||
|
} catch (err) {
|
||||||
|
this.products = []
|
||||||
|
this.error = err?.response?.data || err?.message || 'Urun listesi alinamadi'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async fetchColors (productCode) {
|
||||||
|
const code = String(productCode || '').trim()
|
||||||
|
if (!code) return []
|
||||||
|
|
||||||
|
if (this.colorOptionsByCode[code]) {
|
||||||
|
return this.colorOptionsByCode[code]
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.get('/product-colors', { params: { code } })
|
||||||
|
const data = res?.data
|
||||||
|
const list = Array.isArray(data) ? data : []
|
||||||
|
this.colorOptionsByCode[code] = list
|
||||||
|
return list
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err?.response?.data || err?.message || 'Renk listesi alinamadi'
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async fetchSecondColors (productCode, colorCode) {
|
||||||
|
const code = String(productCode || '').trim()
|
||||||
|
const color = String(colorCode || '').trim()
|
||||||
|
if (!code || !color) return []
|
||||||
|
|
||||||
|
const key = `${code}::${color}`
|
||||||
|
if (this.secondColorOptionsByKey[key]) {
|
||||||
|
return this.secondColorOptionsByKey[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.get('/product-secondcolor', { params: { code, color } })
|
||||||
|
const data = res?.data
|
||||||
|
const list = Array.isArray(data) ? data : []
|
||||||
|
this.secondColorOptionsByKey[key] = list
|
||||||
|
return list
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err?.response?.data || err?.message || '2. renk listesi alinamadi'
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async validateUpdates (orderHeaderID, lines) {
|
||||||
|
if (!orderHeaderID) return { missingCount: 0, missing: [] }
|
||||||
|
|
||||||
|
this.saving = true
|
||||||
|
this.error = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.post(
|
||||||
|
`/orders/production-items/${encodeURIComponent(orderHeaderID)}/validate`,
|
||||||
|
{ lines }
|
||||||
|
)
|
||||||
|
return res?.data || { missingCount: 0, missing: [] }
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err?.response?.data || err?.message || 'Kontrol basarisiz'
|
||||||
|
throw err
|
||||||
|
} finally {
|
||||||
|
this.saving = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async applyUpdates (orderHeaderID, lines, insertMissing) {
|
||||||
|
if (!orderHeaderID) return { updated: 0, inserted: 0 }
|
||||||
|
|
||||||
|
this.saving = true
|
||||||
|
this.error = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.post(
|
||||||
|
`/orders/production-items/${encodeURIComponent(orderHeaderID)}/apply`,
|
||||||
|
{ lines, insertMissing }
|
||||||
|
)
|
||||||
|
return res?.data || { updated: 0, inserted: 0 }
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err?.response?.data || err?.message || 'Guncelleme basarisiz'
|
||||||
|
throw err
|
||||||
|
} finally {
|
||||||
|
this.saving = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
166
ui/src/stores/OrderProductionUpdateStore.js
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
// src/stores/OrderProductionUpdateStore.js
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import api from 'src/services/api'
|
||||||
|
|
||||||
|
let lastRequestId = 0
|
||||||
|
|
||||||
|
export const useOrderProductionUpdateStore = defineStore('orderproductionupdate', {
|
||||||
|
state: () => ({
|
||||||
|
orders: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
|
||||||
|
filters: {
|
||||||
|
search: '',
|
||||||
|
CurrAccCode: '',
|
||||||
|
OrderDate: ''
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
filteredOrders (state) {
|
||||||
|
let result = state.orders
|
||||||
|
|
||||||
|
if (state.filters.CurrAccCode) {
|
||||||
|
result = result.filter(o => o.CurrAccCode === state.filters.CurrAccCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.filters.OrderDate) {
|
||||||
|
result = result.filter(o =>
|
||||||
|
o.OrderDate?.startsWith(state.filters.OrderDate)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
|
||||||
|
totalVisibleUSD () {
|
||||||
|
return this.filteredOrders.reduce((sum, o) => {
|
||||||
|
const value = Number(o.TotalAmountUSD || 0)
|
||||||
|
return sum + (Number.isFinite(value) ? value : 0)
|
||||||
|
}, 0)
|
||||||
|
},
|
||||||
|
|
||||||
|
totalPackedVisibleUSD () {
|
||||||
|
return this.filteredOrders.reduce((sum, o) => {
|
||||||
|
const value = Number(o.PackedUSD || 0)
|
||||||
|
return sum + (Number.isFinite(value) ? value : 0)
|
||||||
|
}, 0)
|
||||||
|
},
|
||||||
|
|
||||||
|
packedVisibleRatePct () {
|
||||||
|
if (!this.totalVisibleUSD) return 0
|
||||||
|
return (this.totalPackedVisibleUSD / this.totalVisibleUSD) * 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async fetchOrders () {
|
||||||
|
|
||||||
|
// ==============================
|
||||||
|
// 📌 REQUEST ID
|
||||||
|
// ==============================
|
||||||
|
const rid = ++lastRequestId
|
||||||
|
|
||||||
|
// ==============================
|
||||||
|
// 📌 SEARCH SNAPSHOT
|
||||||
|
// ==============================
|
||||||
|
const raw = this.filters.search ?? ''
|
||||||
|
const trimmed = String(raw).trim()
|
||||||
|
|
||||||
|
// ==============================
|
||||||
|
// 📌 REQUEST LOG
|
||||||
|
// ==============================
|
||||||
|
console.groupCollapsed(
|
||||||
|
`%c[orders-prod] FETCH rid=${rid}`,
|
||||||
|
'color:#1976d2;font-weight:bold'
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('raw =', JSON.stringify(raw), 'len=', String(raw).length)
|
||||||
|
console.log('trimmed =', JSON.stringify(trimmed), 'len=', trimmed.length)
|
||||||
|
console.log('filters =', JSON.parse(JSON.stringify(this.filters)))
|
||||||
|
console.log('lastRID =', lastRequestId)
|
||||||
|
|
||||||
|
console.groupEnd()
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// ==============================
|
||||||
|
// 📌 PARAMS
|
||||||
|
// ==============================
|
||||||
|
const params = {}
|
||||||
|
if (trimmed) params.search = trimmed
|
||||||
|
|
||||||
|
// ==============================
|
||||||
|
// 📌 API CALL
|
||||||
|
// ==============================
|
||||||
|
const res = await api.get('/orders/production-list', { params })
|
||||||
|
|
||||||
|
// ==============================
|
||||||
|
// 📌 STALE CHECK
|
||||||
|
// ==============================
|
||||||
|
if (rid !== lastRequestId) {
|
||||||
|
console.warn(
|
||||||
|
`[orders-prod] IGNORE stale response rid=${rid} last=${lastRequestId}`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================
|
||||||
|
// 📌 DATA
|
||||||
|
// ==============================
|
||||||
|
const data = res?.data
|
||||||
|
this.orders = Array.isArray(data) ? data : []
|
||||||
|
|
||||||
|
// ==============================
|
||||||
|
// 📌 RESPONSE LOG
|
||||||
|
// ==============================
|
||||||
|
console.groupCollapsed(
|
||||||
|
`%c[orders-prod] RESPONSE rid=${rid} count=${this.orders.length}`,
|
||||||
|
'color:#2e7d32;font-weight:bold'
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('status =', res?.status)
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'sample =',
|
||||||
|
this.orders.slice(0, 5).map(o => ({
|
||||||
|
id: o.OrderHeaderID,
|
||||||
|
no: o.OrderNumber,
|
||||||
|
code: o.CurrAccCode,
|
||||||
|
name: o.CurrAccDescription
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
console.groupEnd()
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
|
||||||
|
if (rid !== lastRequestId) return
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
'[orders-prod] FETCH FAILED',
|
||||||
|
err?.response?.status,
|
||||||
|
err?.response?.data || err
|
||||||
|
)
|
||||||
|
|
||||||
|
this.orders = []
|
||||||
|
|
||||||
|
this.error =
|
||||||
|
err?.response?.data ||
|
||||||
|
err?.message ||
|
||||||
|
'Sipariş listesi alınamadı'
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
|
||||||
|
if (rid === lastRequestId) {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -78,15 +78,7 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
========================================================= */
|
========================================================= */
|
||||||
setSession ({ token, user }) {
|
setSession ({ token, user }) {
|
||||||
this.token = token
|
this.token = token
|
||||||
if (user) {
|
this.user = user || null
|
||||||
// Keep prior role fields if backend returns partial user payload.
|
|
||||||
this.user = {
|
|
||||||
...(this.user || {}),
|
|
||||||
...user
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.user = null
|
|
||||||
}
|
|
||||||
this.forcePasswordChange = !!user?.force_password_change
|
this.forcePasswordChange = !!user?.force_password_change
|
||||||
|
|
||||||
localStorage.setItem('token', token)
|
localStorage.setItem('token', token)
|
||||||
|
|||||||
@@ -43,11 +43,11 @@ export function buildComboKey(row, beden) {
|
|||||||
|
|
||||||
|
|
||||||
export const BEDEN_SCHEMA = [
|
export const BEDEN_SCHEMA = [
|
||||||
|
{ key: 'tak', title: 'TAKIM ELBISE', values: ['44','46','48','50','52','54','56','58','60','62','64','66','68','70','72','74'] },
|
||||||
{ key: 'ayk', title: 'AYAKKABI', values: ['39','40','41','42','43','44','45'] },
|
{ key: 'ayk', title: 'AYAKKABI', values: ['39','40','41','42','43','44','45'] },
|
||||||
{ key: 'yas', title: 'YAS', values: ['2','4','6','8','10','12','14'] },
|
{ key: 'yas', title: 'YAS', values: ['2','4','6','8','10','12','14'] },
|
||||||
{ key: 'pan', title: 'PANTOLON', values: ['38','40','42','44','46','48','50','52','54','56','58','60','62','64','66','68'] },
|
{ key: 'pan', title: 'PANTOLON', values: ['38','40','42','44','46','48','50','52','54','56','58','60','62','64','66','68'] },
|
||||||
{ key: 'gom', title: 'GOMLEK', values: ['XS','S','M','L','XL','2XL','3XL','4XL','5XL','6XL','7XL'] },
|
{ key: 'gom', title: 'GOMLEK', values: ['XS','S','M','L','XL','2XL','3XL','4XL','5XL','6XL','7XL'] },
|
||||||
{ key: 'tak', title: 'TAKIM ELBISE', values: ['44','46','48','50','52','54','56','58','60','62','64','66','68','70','72','74'] },
|
|
||||||
{ key: 'aksbir', title: 'AKSESUAR', values: [' ', '44', 'STD', '110', '115', '120', '125', '130', '135'] }
|
{ key: 'aksbir', title: 'AKSESUAR', values: [' ', '44', 'STD', '110', '115', '120', '125', '130', '135'] }
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1705,6 +1705,7 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
rows[idx] = {
|
rows[idx] = {
|
||||||
...prev,
|
...prev,
|
||||||
...newRow,
|
...newRow,
|
||||||
|
_dirty: true,
|
||||||
id: prev.id,
|
id: prev.id,
|
||||||
OrderLineID: prev.OrderLineID || null,
|
OrderLineID: prev.OrderLineID || null,
|
||||||
lineIdMap: preservedLineIdMap
|
lineIdMap: preservedLineIdMap
|
||||||
@@ -1763,6 +1764,7 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
|
|
||||||
const insertedRow = {
|
const insertedRow = {
|
||||||
...newRow,
|
...newRow,
|
||||||
|
_dirty: true,
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
OrderLineID: null,
|
OrderLineID: null,
|
||||||
lineIdMap: {}
|
lineIdMap: {}
|
||||||
@@ -1838,6 +1840,7 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
rows[dupIdx] = {
|
rows[dupIdx] = {
|
||||||
...prev,
|
...prev,
|
||||||
...newRow,
|
...newRow,
|
||||||
|
_dirty: true,
|
||||||
|
|
||||||
// kritik korumalar
|
// kritik korumalar
|
||||||
id: prev.id,
|
id: prev.id,
|
||||||
@@ -1871,6 +1874,7 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
// dup yoksa (veya dup delete satırıydı) → yeni satır
|
// dup yoksa (veya dup delete satırıydı) → yeni satır
|
||||||
rows.push({
|
rows.push({
|
||||||
...newRow,
|
...newRow,
|
||||||
|
_dirty: true,
|
||||||
id: newRow.id || crypto.randomUUID(),
|
id: newRow.id || crypto.randomUUID(),
|
||||||
OrderLineID: null,
|
OrderLineID: null,
|
||||||
lineIdMap: { ...(newRow.lineIdMap || {}) }
|
lineIdMap: { ...(newRow.lineIdMap || {}) }
|
||||||
@@ -2529,6 +2533,50 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
console.log(`🧭 Order mode set edildi → ${mode}`)
|
console.log(`🧭 Order mode set edildi → ${mode}`)
|
||||||
}
|
}
|
||||||
,
|
,
|
||||||
|
// Sync only header-related fields from the form before submit.
|
||||||
|
syncHeaderFromForm(form) {
|
||||||
|
if (!form || typeof form !== 'object') return
|
||||||
|
|
||||||
|
const keys = [
|
||||||
|
'OrderHeaderID',
|
||||||
|
'OrderTypeCode',
|
||||||
|
'ProcessCode',
|
||||||
|
'OrderNumber',
|
||||||
|
'OrderDate',
|
||||||
|
'AverageDueDate',
|
||||||
|
'Description',
|
||||||
|
'InternalDescription',
|
||||||
|
'CurrAccTypeCode',
|
||||||
|
'CurrAccCode',
|
||||||
|
'CurrAccDescription',
|
||||||
|
'DocCurrencyCode',
|
||||||
|
'LocalCurrencyCode',
|
||||||
|
'ExchangeRate',
|
||||||
|
'OfficeCode',
|
||||||
|
'CreatedUserName',
|
||||||
|
'CreatedDate',
|
||||||
|
'LastUpdatedUserName',
|
||||||
|
'LastUpdatedDate',
|
||||||
|
'PaymentTerm',
|
||||||
|
'WarehouseCode',
|
||||||
|
'StoreCode'
|
||||||
|
]
|
||||||
|
|
||||||
|
const patch = {}
|
||||||
|
for (const k of keys) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(form, k)) {
|
||||||
|
patch[k] = form[k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(patch).length > 0) {
|
||||||
|
this.header = {
|
||||||
|
...(this.header || {}),
|
||||||
|
...patch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
,
|
||||||
/* ===========================================================
|
/* ===========================================================
|
||||||
🟦 submitAllReal (v12.1c — FINAL / CLEAN + PRE-VALIDATE)
|
🟦 submitAllReal (v12.1c — FINAL / CLEAN + PRE-VALIDATE)
|
||||||
-----------------------------------------------------------
|
-----------------------------------------------------------
|
||||||
@@ -2549,6 +2597,9 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
// 🔒 Kontrollü submit → route leave guard susar
|
// 🔒 Kontrollü submit → route leave guard susar
|
||||||
this.isControlledSubmit = true
|
this.isControlledSubmit = true
|
||||||
|
|
||||||
|
// ✅ Formdaki header alanlarını store'a taşı
|
||||||
|
this.syncHeaderFromForm?.(form)
|
||||||
|
|
||||||
const isNew = this.mode === 'new'
|
const isNew = this.mode === 'new'
|
||||||
const { header, lines } = this.buildFinalOrderJson()
|
const { header, lines } = this.buildFinalOrderJson()
|
||||||
|
|
||||||
@@ -2633,7 +2684,12 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
// 🧪 PRE-VALIDATE — prItemVariant ön kontrol
|
// 🧪 PRE-VALIDATE — prItemVariant ön kontrol
|
||||||
// - invalid varsa CREATE/UPDATE ÇALIŞMAZ
|
// - invalid varsa CREATE/UPDATE ÇALIŞMAZ
|
||||||
// =======================================================
|
// =======================================================
|
||||||
const v = await api.post('/order/validate', { header, lines })
|
const linesToValidate =
|
||||||
|
isNew
|
||||||
|
? lines
|
||||||
|
: lines.filter(l => l._deleteSignal === true || l._dirty === true || !l.OrderLineID)
|
||||||
|
|
||||||
|
const v = await api.post('/order/validate', { header, lines: linesToValidate })
|
||||||
const invalid = v?.data?.invalid || []
|
const invalid = v?.data?.invalid || []
|
||||||
|
|
||||||
if (invalid.length > 0) {
|
if (invalid.length > 0) {
|
||||||
@@ -2973,6 +3029,8 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
OrderLineID: orderLineId || '',
|
OrderLineID: orderLineId || '',
|
||||||
ClientKey: makeLineClientKey(row, grpKey, bedenKey),
|
ClientKey: makeLineClientKey(row, grpKey, bedenKey),
|
||||||
ComboKey: comboKey,
|
ComboKey: comboKey,
|
||||||
|
_dirty: row?._dirty === true,
|
||||||
|
_deleteSignal: isDeleteSignal === true,
|
||||||
|
|
||||||
SortOrder: 0,
|
SortOrder: 0,
|
||||||
ItemTypeCode: 1,
|
ItemTypeCode: 1,
|
||||||
@@ -3054,12 +3112,16 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
if (orderLineId && !existing.OrderLineID) {
|
if (orderLineId && !existing.OrderLineID) {
|
||||||
existing.OrderLineID = orderLineId
|
existing.OrderLineID = orderLineId
|
||||||
}
|
}
|
||||||
|
existing._deleteSignal = true
|
||||||
existing.Qty1 = 0
|
existing.Qty1 = 0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/* MERGE */
|
/* MERGE */
|
||||||
existing.Qty1 += qty
|
existing.Qty1 += qty
|
||||||
|
if (row?._dirty === true) {
|
||||||
|
existing._dirty = true
|
||||||
|
}
|
||||||
|
|
||||||
if (this.mode === 'edit' && orderLineId && !existing.OrderLineID) {
|
if (this.mode === 'edit' && orderLineId && !existing.OrderLineID) {
|
||||||
existing.OrderLineID = orderLineId
|
existing.OrderLineID = orderLineId
|
||||||
@@ -3346,66 +3408,56 @@ export function normalizeBeden(v) {
|
|||||||
- Keeps frontend aksbir bucket for accessory lines
|
- Keeps frontend aksbir bucket for accessory lines
|
||||||
=========================================================== */
|
=========================================================== */
|
||||||
export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '') {
|
export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '') {
|
||||||
const list = Array.isArray(bedenList) ? bedenList : []
|
const list = Array.isArray(bedenList) && bedenList.length > 0
|
||||||
const ana = normalizeTextForMatch(urunAnaGrubu)
|
? bedenList.map(v => (v || '').toString().trim().toUpperCase())
|
||||||
const alt = normalizeTextForMatch(urunKategori)
|
: [' ']
|
||||||
|
|
||||||
// Frontend compatibility: accessory-only products should stay in aksbir.
|
const ana = (urunAnaGrubu || '')
|
||||||
const accessoryGroups = [
|
.toUpperCase()
|
||||||
'AKSESUAR', 'KRAVAT', 'PAPYON', 'KEMER', 'CORAP',
|
.trim()
|
||||||
'FULAR', 'MENDIL', 'KASKOL', 'ASKI', 'YAKA', 'KOL DUGMESI'
|
.replace(/\(.*?\)/g, '')
|
||||||
|
.replace(/[^A-ZÇĞİÖŞÜ0-9\s]/g, '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
|
||||||
|
const kat = (urunKategori || '').toUpperCase().trim()
|
||||||
|
// 🔸 Aksesuar ise "aksbir"
|
||||||
|
const aksesuarGruplari = [
|
||||||
|
'AKSESUAR','KRAVAT','PAPYON','KEMER','CORAP','ÇORAP',
|
||||||
|
'FULAR','MENDIL','MENDİL','KASKOL','ASKI',
|
||||||
|
'YAKA','KOL DUGMESI','KOL DÜĞMESİ'
|
||||||
]
|
]
|
||||||
const clothingGroups = ['GOMLEK', 'CEKET', 'PANTOLON', 'MONT', 'YELEK', 'TAKIM', 'TSHIRT']
|
const giyimGruplari = ['GÖMLEK','CEKET','PANTOLON','MONT','YELEK','TAKIM','TSHIRT','TİŞÖRT']
|
||||||
|
// 🔸 Pantolon özel durumu
|
||||||
if (
|
if (
|
||||||
accessoryGroups.some(g => ana.includes(g) || alt.includes(g)) &&
|
aksesuarGruplari.some(g => ana.includes(g) || kat.includes(g)) &&
|
||||||
!clothingGroups.some(g => ana.includes(g))
|
!giyimGruplari.some(g => ana.includes(g))
|
||||||
) {
|
) return 'aksbir'
|
||||||
return 'aksbir'
|
|
||||||
|
if (ana.includes('PANTOLON') && kat.includes('YETİŞKİN')) return 'pan'
|
||||||
|
// 🔸 Tamamen numerik (örneğin 39-44 arası) → ayakkabı
|
||||||
|
const allNumeric = list.every(v => /^\d+$/.test(v))
|
||||||
|
if (allNumeric) {
|
||||||
|
const nums = list.map(v => parseInt(v, 10)).filter(Boolean)
|
||||||
|
const diffs = nums.slice(1).map((v, i) => v - nums[i])
|
||||||
|
if (diffs.every(d => d === 1) && nums[0] >= 35 && nums[0] <= 46) return 'ayk'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ana.includes('AYAKKABI') || alt.includes('AYAKKABI')) {
|
// 🔸 Yaş grubu (çocuk/garson)
|
||||||
return 'ayk'
|
if (kat.includes('GARSON') || kat.includes('ÇOCUK')) return 'yas'
|
||||||
}
|
|
||||||
|
|
||||||
let hasYasNumeric = false
|
// 🔸 Harfli beden varsa doğrudan "gom" (gömlek, üst giyim)
|
||||||
let hasAykNumeric = false
|
const harfliBedenler = ['XS','S','M','L','XL','XXL','3XL','4XL']
|
||||||
let hasPanNumeric = false
|
if (list.some(b => harfliBedenler.includes(b))) return 'gom'
|
||||||
|
|
||||||
for (const raw of list) {
|
|
||||||
const b = safeTrimUpperJs(raw)
|
|
||||||
|
|
||||||
switch (b) {
|
|
||||||
case 'XS':
|
|
||||||
case 'S':
|
|
||||||
case 'M':
|
|
||||||
case 'L':
|
|
||||||
case 'XL':
|
|
||||||
case '2XL':
|
|
||||||
case '3XL':
|
|
||||||
case '4XL':
|
|
||||||
case '5XL':
|
|
||||||
case '6XL':
|
|
||||||
case '7XL':
|
|
||||||
return 'gom'
|
|
||||||
}
|
|
||||||
|
|
||||||
const n = parseNumericSizeJs(b)
|
|
||||||
if (n == null) continue
|
|
||||||
|
|
||||||
if (n >= 2 && n <= 14) hasYasNumeric = true
|
|
||||||
if (n >= 39 && n <= 45) hasAykNumeric = true
|
|
||||||
if (n >= 38 && n <= 68) hasPanNumeric = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasAykNumeric) return 'ayk'
|
|
||||||
if (ana.includes('PANTOLON')) return 'pan'
|
|
||||||
if (hasPanNumeric) return 'pan'
|
|
||||||
if (alt.includes('COCUK') || alt.includes('GARSON')) return 'yas'
|
|
||||||
if (hasYasNumeric) return 'yas'
|
|
||||||
|
|
||||||
|
// 🔸 Varsayılan: takım elbise
|
||||||
return 'tak'
|
return 'tak'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function toSummaryRowFromForm(form) {
|
export function toSummaryRowFromForm(form) {
|
||||||
if (!form) return null
|
if (!form) return null
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|
||||||
|
|||||||