Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-05-14 02:19:59 +03:00
parent 43f965a3cf
commit 7d1304b75a
8 changed files with 726 additions and 109 deletions

View File

@@ -21,7 +21,8 @@ UI_DIR=/opt/bssapp/ui/dist
# =============================== # ===============================
# DATABASES # DATABASES
# =============================== # ===============================
POSTGRES_CONN=host=46.224.33.150 port=5432 user=postgres password=tayitkan dbname=baggib2b sslmode=disable # Local dev: connect via SSH tunnel to the server (Windows -> 15432 -> server 127.0.0.1:5432).
POSTGRES_CONN=host=127.0.0.1 port=15432 user=postgres password=tayitkan dbname=baggib2b sslmode=disable
MSSQL_CONN=sqlserver://sa:Gil_0150@10.0.0.9:1433?database=BAGGI_V3&encrypt=disable MSSQL_CONN=sqlserver://sa:Gil_0150@10.0.0.9:1433?database=BAGGI_V3&encrypt=disable
URETIM_MSSQL_CONN=sqlserver://sa:Gil_0150@10.0.0.9:1433?database=URETIM&encrypt=disable URETIM_MSSQL_CONN=sqlserver://sa:Gil_0150@10.0.0.9:1433?database=URETIM&encrypt=disable

View File

@@ -16,6 +16,7 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"runtime"
"runtime/debug" "runtime/debug"
"strings" "strings"
@@ -906,6 +907,12 @@ func main() {
log.Println("⚠️ .env / mail.env bulunamadı") log.Println("⚠️ .env / mail.env bulunamadı")
} }
// Local dev convenience: on Windows we generally want to override .env with .env.local
// (e.g. SSH-tunnel PostgreSQL on 127.0.0.1:15432).
if runtime.GOOS == "windows" {
_ = godotenv.Overload(".env.local")
}
jwtSecret := os.Getenv("JWT_SECRET") jwtSecret := os.Getenv("JWT_SECRET")
if len(jwtSecret) < 10 { if len(jwtSecret) < 10 {
log.Fatal("❌ JWT_SECRET tanımlı değil veya çok kısa (min 10 karakter)") log.Fatal("❌ JWT_SECRET tanımlı değil veya çok kısa (min 10 karakter)")

View File

@@ -98,6 +98,7 @@ type ProductionHasCostDetailHeader struct {
NOnMLNo string `json:"nOnMLNo"` NOnMLNo string `json:"nOnMLNo"`
UrunKodu string `json:"UrunKodu"` UrunKodu string `json:"UrunKodu"`
UrunAdi string `json:"UrunAdi"` UrunAdi string `json:"UrunAdi"`
UrunIlkGrubu string `json:"UrunIlkGrubu"`
UrunAnaGrubu string `json:"UrunAnaGrubu"` UrunAnaGrubu string `json:"UrunAnaGrubu"`
UrunAltGrubu string `json:"UrunAltGrubu"` UrunAltGrubu string `json:"UrunAltGrubu"`
UretimSekliID string `json:"UretimSekliID"` UretimSekliID string `json:"UretimSekliID"`
@@ -131,6 +132,7 @@ type ProductionHasCostDetailEditorOption struct {
NHammaddeTuruNo string `json:"nHammaddeTuruNo"` NHammaddeTuruNo string `json:"nHammaddeTuruNo"`
SHammaddeTuruAdi string `json:"sHammaddeTuruAdi"` SHammaddeTuruAdi string `json:"sHammaddeTuruAdi"`
SAciklama3 string `json:"sAciklama3"` SAciklama3 string `json:"sAciklama3"`
MTUrtMTBolumID int `json:"mtUrtMTBolumID"`
SKodu string `json:"sKodu"` SKodu string `json:"sKodu"`
SAciklama string `json:"sAciklama"` SAciklama string `json:"sAciklama"`
SModel string `json:"sModel"` SModel string `json:"sModel"`

View File

@@ -37,6 +37,34 @@ ORDER BY ProductCode;
return urunAnaGrubu, urunAltGrubu, nil return urunAnaGrubu, urunAltGrubu, nil
} }
func GetProductIlkAnaAltGrupByUrunKodu(ctx context.Context, mssqlDB *sql.DB, urunKodu string) (urunIlkGrubu string, urunAnaGrubu string, urunAltGrubu string, err error) {
urunKodu = strings.TrimSpace(urunKodu)
if mssqlDB == nil || urunKodu == "" {
return "", "", "", nil
}
// Nebim V3: ProductFilterWithDescription exposes ProductAtt42Desc (ilk grup), ProductAtt01Desc (ana), ProductAtt02Desc (alt).
sqlText := `
SELECT TOP 1
ISNULL(ProductAtt42Desc, '') AS UrunIlkGrubu,
ISNULL(ProductAtt01Desc, '') AS UrunAnaGrubu,
ISNULL(ProductAtt02Desc, '') AS UrunAltGrubu
FROM ProductFilterWithDescription('TR')
WHERE IsBlocked = 0
AND LTRIM(RTRIM(ProductCode)) = @p1
ORDER BY ProductCode;
`
row := mssqlDB.QueryRowContext(ctx, sqlText, urunKodu)
if err := row.Scan(&urunIlkGrubu, &urunAnaGrubu, &urunAltGrubu); err != nil {
if err == sql.ErrNoRows {
return "", "", "", nil
}
return "", "", "", err
}
return urunIlkGrubu, urunAnaGrubu, urunAltGrubu, nil
}
func GetProductionProductCostingAnaGrupOptions(ctx context.Context, mssqlDB *sql.DB, search string, limit int) (*sql.Rows, error) { func GetProductionProductCostingAnaGrupOptions(ctx context.Context, mssqlDB *sql.DB, search string, limit int) (*sql.Rows, error) {
search = strings.TrimSpace(search) search = strings.TrimSpace(search)
if limit <= 0 { if limit <= 0 {
@@ -131,6 +159,7 @@ SELECT TOP (@p2)
ISNULL(B.sAdi, '') AS sAdi ISNULL(B.sAdi, '') AS sAdi
FROM dbo.spUrtMTBolum B WITH (NOLOCK) FROM dbo.spUrtMTBolum B WITH (NOLOCK)
WHERE ISNULL(B.bAktif, 0) = 1 WHERE ISNULL(B.bAktif, 0) = 1
AND ISNULL(B.nUrtTipiID, 0) = 1
AND (@p1 = '' OR ISNULL(B.sAdi, '') LIKE @p3 OR CONVERT(VARCHAR(32), ISNULL(B.nUrtMTBolumID, 0)) LIKE @p3) AND (@p1 = '' OR ISNULL(B.sAdi, '') LIKE @p3 OR CONVERT(VARCHAR(32), ISNULL(B.nUrtMTBolumID, 0)) LIKE @p3)
ORDER BY B.nUrtMTBolumID ORDER BY B.nUrtMTBolumID
` `
@@ -160,6 +189,7 @@ SELECT
FROM dbo.mk_MaliyetParcaEslestirme M WITH (NOLOCK) FROM dbo.mk_MaliyetParcaEslestirme M WITH (NOLOCK)
LEFT JOIN dbo.spUrtMTBolum B WITH (NOLOCK) LEFT JOIN dbo.spUrtMTBolum B WITH (NOLOCK)
ON B.nUrtMTBolumID = M.nUrtMTBolumID ON B.nUrtMTBolumID = M.nUrtMTBolumID
AND ISNULL(B.nUrtTipiID, 0) = 1
OUTER APPLY ( OUTER APPLY (
SELECT SELECT
STUFF(( STUFF((
@@ -268,6 +298,97 @@ DECLARE @id INT;
return mappingID, nil return mappingID, nil
} }
type ProductionProductCostingInvalidHammaddeForPart struct {
NHammaddeTuruNo int
Aciklama string
MTBolumID int
}
// FilterHammaddeTurleriForPart enforces spUrtOnMLHammaddeTuru.MTnUrtMTBolumID rules:
// - If MTnUrtMTBolumID > 0, the hammadde type is allowed only for that part (nUrtMTBolumID).
// - If MTnUrtMTBolumID is 0/NULL, it is considered global/unknown and allowed.
func FilterHammaddeTurleriForPart(
ctx context.Context,
uretimDB *sql.DB,
nUrtMTBolumID int,
nHammaddeTurleri []int,
) (allowed []int, invalid []ProductionProductCostingInvalidHammaddeForPart, err error) {
seen := make(map[int]bool, len(nHammaddeTurleri))
unique := make([]int, 0, len(nHammaddeTurleri))
for _, n := range nHammaddeTurleri {
if n <= 0 || seen[n] {
continue
}
seen[n] = true
unique = append(unique, n)
}
if len(unique) == 0 {
return []int{}, []ProductionProductCostingInvalidHammaddeForPart{}, nil
}
// Build IN list safely.
ph := make([]string, 0, len(unique))
args := make([]any, 0, len(unique))
for i, n := range unique {
ph = append(ph, fmt.Sprintf("@p%d", i+1))
args = append(args, n)
}
sqlText := fmt.Sprintf(`
SELECT
ISNULL(H.nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
ISNULL(H.sAciklama, '') AS sAciklama,
ISNULL(H.MTnUrtMTBolumID, 0) AS MTnUrtMTBolumID
FROM dbo.spUrtOnMLHammaddeTuru H WITH (NOLOCK)
WHERE H.nHammaddeTuruNo IN (%s)
`, strings.Join(ph, ","))
rows, qerr := uretimDB.QueryContext(ctx, sqlText, args...)
if qerr != nil {
return nil, nil, qerr
}
defer rows.Close()
type meta struct {
aciklama string
mtBolumID int
}
metaByNo := map[int]meta{}
for rows.Next() {
var no int
var aciklama string
var mtBolum int
if scanErr := rows.Scan(&no, &aciklama, &mtBolum); scanErr != nil {
return nil, nil, scanErr
}
metaByNo[no] = meta{aciklama: strings.TrimSpace(aciklama), mtBolumID: mtBolum}
}
if rowsErr := rows.Err(); rowsErr != nil {
return nil, nil, rowsErr
}
allowed = make([]int, 0, len(unique))
invalid = make([]ProductionProductCostingInvalidHammaddeForPart, 0)
for _, n := range unique {
m, ok := metaByNo[n]
if !ok {
// Unknown in lookup; allow (we can't validate).
allowed = append(allowed, n)
continue
}
if m.mtBolumID > 0 && nUrtMTBolumID > 0 && m.mtBolumID != nUrtMTBolumID {
invalid = append(invalid, ProductionProductCostingInvalidHammaddeForPart{
NHammaddeTuruNo: n,
Aciklama: m.aciklama,
MTBolumID: m.mtBolumID,
})
continue
}
allowed = append(allowed, n)
}
return allowed, invalid, nil
}
func SetProductionProductCostingParcaMappingActive(ctx context.Context, uretimDB *sql.DB, id int, bAktif bool, user string) error { func SetProductionProductCostingParcaMappingActive(ctx context.Context, uretimDB *sql.DB, id int, bAktif bool, user string) error {
user = strings.TrimSpace(user) user = strings.TrimSpace(user)
activeVal := 0 activeVal := 0
@@ -699,7 +820,8 @@ WITH RecipeMatch AS (
), ),
HammaddeTekil AS ( HammaddeTekil AS (
SELECT SELECT
ISNULL(NULLIF(LTRIM(RTRIM(HT.sAciklama3)), ''), ISNULL(NULLIF(LTRIM(RTRIM(HT.sAciklama)), ''), N'TANIMSIZ')) AS sAciklama3, -- Group label: DT/TP/CM2/FABRIC... Prefer sAciklama3, then sAciklama2. Never fall back to sAciklama (name).
COALESCE(NULLIF(LTRIM(RTRIM(HT.sAciklama3)), ''), NULLIF(LTRIM(RTRIM(HT.sAciklama2)), ''), N'TANIMSIZ') AS sAciklama3,
ISNULL(HT.nHammaddeTuruNo, 0) AS nHammaddeTuruNoSort, ISNULL(HT.nHammaddeTuruNo, 0) AS nHammaddeTuruNoSort,
RTRIM(CONVERT(VARCHAR(32), ISNULL(HT.nHammaddeTuruNo, 0))) AS nHammaddeTuruNo, RTRIM(CONVERT(VARCHAR(32), ISNULL(HT.nHammaddeTuruNo, 0))) AS nHammaddeTuruNo,
-- Match URETIM's sp_pUrtOnMaliyetRecetedenKop behavior: use model code + color code instead of variant stock code. -- Match URETIM's sp_pUrtOnMaliyetRecetedenKop behavior: use model code + color code instead of variant stock code.
@@ -709,6 +831,8 @@ HammaddeTekil AS (
ISNULL(HT.sAciklama, '') AS sHammaddeTuruAdi, ISNULL(HT.sAciklama, '') AS sHammaddeTuruAdi,
ISNULL(S.sBirimCinsi1, '') AS sBirim, ISNULL(S.sBirimCinsi1, '') AS sBirim,
ISNULL(RMik.lHMiktar, 0) AS lMiktar, ISNULL(RMik.lHMiktar, 0) AS lMiktar,
ISNULL(HT.MTnUrtMTBolumID, 0) AS MTnUrtMTBolumID,
ISNULL(B.sAdi, '') AS sParcaAdi,
ROW_NUMBER() OVER ( ROW_NUMBER() OVER (
PARTITION BY HT.nHammaddeTuruNo PARTITION BY HT.nHammaddeTuruNo
ORDER BY ISNULL(S.sModel, ISNULL(S.sKodu, '')) ORDER BY ISNULL(S.sModel, ISNULL(S.sKodu, ''))
@@ -725,13 +849,20 @@ HammaddeTekil AS (
SELECT TOP 1 SELECT TOP 1
H.nHammaddeTuruNo, H.nHammaddeTuruNo,
H.sAciklama, H.sAciklama,
H.sAciklama3 H.sAciklama2,
H.sAciklama3,
H.MTnUrtMTBolumID
FROM dbo.spUrtOnMLHammaddeTuru H FROM dbo.spUrtOnMLHammaddeTuru H
WHERE H.nUrtMBolumID = RMik.nUrtMBolumID -- In our data, RMik.nUrtMBolumID carries the required hammadde type number (e.g. 900/901/3300),
-- not a "bolum id". So match by hammadde type number.
WHERE H.nHammaddeTuruNo = RMik.nUrtMBolumID
ORDER BY ORDER BY
CASE WHEN H.nUrtMTBolumID = RMik.nUrtMTBolumID THEN 0 ELSE 1 END, CASE WHEN H.MTnUrtMTBolumID = RMik.nUrtMTBolumID THEN 0 ELSE 1 END,
H.nHammaddeTuruNo H.nHammaddeTuruNo
) HT ) HT
LEFT JOIN dbo.spUrtMTBolum B WITH (NOLOCK)
ON B.nUrtMTBolumID = HT.MTnUrtMTBolumID
AND ISNULL(B.nUrtTipiID, 0) = 1
WHERE HT.nHammaddeTuruNo IS NOT NULL WHERE HT.nHammaddeTuruNo IS NOT NULL
) )
SELECT SELECT
@@ -762,7 +893,7 @@ SELECT
0.0 AS gbpTutar, 0.0 AS gbpTutar,
HT.sBirim, HT.sBirim,
HT.sHammaddeTuruAdi, HT.sHammaddeTuruAdi,
HT.sHammaddeTuruAdi AS sParcaAdi HT.sParcaAdi AS sParcaAdi
FROM HammaddeTekil HT FROM HammaddeTekil HT
WHERE HT.rn = 1 WHERE HT.rn = 1
ORDER BY ORDER BY
@@ -800,15 +931,22 @@ func GetProductionHasCostDetailHammaddeTypeOptions(
SELECT TOP (@p2) SELECT TOP (@p2)
RTRIM(CONVERT(VARCHAR(32), ISNULL(T.nHammaddeTuruNo, 0))) AS nHammaddeTuruNo, RTRIM(CONVERT(VARCHAR(32), ISNULL(T.nHammaddeTuruNo, 0))) AS nHammaddeTuruNo,
ISNULL(T.sAciklama, '') AS sHammaddeTuruAdi, ISNULL(T.sAciklama, '') AS sHammaddeTuruAdi,
ISNULL(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), N'TANIMSIZ') AS sAciklama3 COALESCE(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), NULLIF(LTRIM(RTRIM(T.sAciklama2)), ''), N'TANIMSIZ') AS sAciklama3,
FROM dbo.spUrtOnMLHammaddeTuru T ISNULL(T.MTnUrtMTBolumID, 0) AS mtUrtMTBolumID,
WHERE ISNULL(B.sAdi, '') AS sParcaAdi
FROM dbo.spUrtOnMLHammaddeTuru T WITH (NOLOCK)
LEFT JOIN dbo.spUrtMTBolum B WITH (NOLOCK)
ON B.nUrtMTBolumID = T.MTnUrtMTBolumID
AND ISNULL(B.nUrtTipiID, 0) = 1
WHERE
ISNULL(T.bAktif, 0) = 1 ISNULL(T.bAktif, 0) = 1
AND ( AND (
@p1 = '' @p1 = ''
OR RTRIM(CONVERT(VARCHAR(32), ISNULL(T.nHammaddeTuruNo, 0))) LIKE @p3 OR RTRIM(CONVERT(VARCHAR(32), ISNULL(T.nHammaddeTuruNo, 0))) LIKE @p3
OR ISNULL(T.sAciklama, '') LIKE @p3 OR ISNULL(T.sAciklama, '') LIKE @p3
OR ISNULL(T.sAciklama2, '') LIKE @p3
OR ISNULL(T.sAciklama3, '') LIKE @p3 OR ISNULL(T.sAciklama3, '') LIKE @p3
OR ISNULL(B.sAdi, '') LIKE @p3
) )
ORDER BY ORDER BY
CASE CASE

View File

@@ -596,10 +596,11 @@ func GetProductionHasCostDetailHeaderHandler(w http.ResponseWriter, r *http.Requ
logger.Info("request done", "n_urt_recete_id", item.NUrtReceteID, "urun_kodu", item.UrunKodu) logger.Info("request done", "n_urt_recete_id", item.NUrtReceteID, "urun_kodu", item.UrunKodu)
if mssqlDB != nil { if mssqlDB != nil {
ana, alt, err := queries.GetProductAnaAltGrupByUrunKodu(ctx, mssqlDB, item.UrunKodu) ilk, ana, alt, err := queries.GetProductIlkAnaAltGrupByUrunKodu(ctx, mssqlDB, item.UrunKodu)
if err != nil { if err != nil {
logger.Warn("product group query error", "err", err) logger.Warn("product group query error", "err", err)
} else { } else {
item.UrunIlkGrubu = ilk
item.UrunAnaGrubu = ana item.UrunAnaGrubu = ana
item.UrunAltGrubu = alt item.UrunAltGrubu = alt
} }
@@ -659,10 +660,11 @@ func GetProductionHasCostDetailHeaderHandler(w http.ResponseWriter, r *http.Requ
logger.Info("request done", "n_onml_no", item.NOnMLNo, "urun_kodu", item.UrunKodu, "n_urt_recete_id", item.NUrtReceteID) logger.Info("request done", "n_onml_no", item.NOnMLNo, "urun_kodu", item.UrunKodu, "n_urt_recete_id", item.NUrtReceteID)
if mssqlDB != nil { if mssqlDB != nil {
ana, alt, err := queries.GetProductAnaAltGrupByUrunKodu(ctx, mssqlDB, item.UrunKodu) ilk, ana, alt, err := queries.GetProductIlkAnaAltGrupByUrunKodu(ctx, mssqlDB, item.UrunKodu)
if err != nil { if err != nil {
logger.Warn("product group query error", "err", err) logger.Warn("product group query error", "err", err)
} else { } else {
item.UrunIlkGrubu = ilk
item.UrunAnaGrubu = ana item.UrunAnaGrubu = ana
item.UrunAltGrubu = alt item.UrunAltGrubu = alt
} }
@@ -761,7 +763,7 @@ func GetProductionHasCostDetailEditorOptionsHandler(w http.ResponseWriter, r *ht
list := make([]models.ProductionHasCostDetailEditorOption, 0, limit) list := make([]models.ProductionHasCostDetailEditorOption, 0, limit)
for rows.Next() { for rows.Next() {
var item models.ProductionHasCostDetailEditorOption var item models.ProductionHasCostDetailEditorOption
if err := rows.Scan(&item.NHammaddeTuruNo, &item.SHammaddeTuruAdi, &item.SAciklama3); err != nil { if err := rows.Scan(&item.NHammaddeTuruNo, &item.SHammaddeTuruAdi, &item.SAciklama3, &item.MTUrtMTBolumID, &item.SParcaAdi); err != nil {
logger.Warn("hammadde scan error", "err", err) logger.Warn("hammadde scan error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] hammadde scan error: %v", err) log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] hammadde scan error: %v", err)
continue continue
@@ -769,7 +771,6 @@ func GetProductionHasCostDetailEditorOptionsHandler(w http.ResponseWriter, r *ht
item.Kind = "hammadde" item.Kind = "hammadde"
item.Value = item.NHammaddeTuruNo item.Value = item.NHammaddeTuruNo
item.Label = strings.TrimSpace(item.NHammaddeTuruNo + " - " + item.SHammaddeTuruAdi) item.Label = strings.TrimSpace(item.NHammaddeTuruNo + " - " + item.SHammaddeTuruAdi)
item.SParcaAdi = item.SAciklama3
list = append(list, item) list = append(list, item)
} }
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
@@ -1958,7 +1959,18 @@ func PostProductionProductCostingParcaMappingUpsertHandler(w http.ResponseWriter
return return
} }
id, err := queries.UpsertProductionProductCostingParcaMapping(ctx, uretimDB, req.UrunIlkGrubu, req.UrunAnaGrubu, req.UrunAltGrubu, req.NUrtMTBolumID, req.NHammaddeTurleri, req.BAktif, user) allowed, invalid, ferr := queries.FilterHammaddeTurleriForPart(ctx, uretimDB, req.NUrtMTBolumID, req.NHammaddeTurleri)
if ferr != nil {
logger.Error("hammadde validation error", "err", ferr)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
// Soft-enforce: drop invalid types instead of hard failing. This prevents a "multi-part save" from failing
// when the UI reuses the same hammadde list across multiple MT bolum selections.
// We still return the ignored list so UI/logs can surface it.
ignored := invalid
id, err := queries.UpsertProductionProductCostingParcaMapping(ctx, uretimDB, req.UrunIlkGrubu, req.UrunAnaGrubu, req.UrunAltGrubu, req.NUrtMTBolumID, allowed, req.BAktif, user)
if err != nil { if err != nil {
logger.Error("exec error", "err", err) logger.Error("exec error", "err", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
@@ -1966,7 +1978,10 @@ func PostProductionProductCostingParcaMappingUpsertHandler(w http.ResponseWriter
} }
logger.Info("request done", "id", id, "user", user, "bAktif", req.BAktif) logger.Info("request done", "id", id, "user", user, "bAktif", req.BAktif)
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "id": id}) if len(ignored) > 0 {
logger.Warn("hammadde types ignored due to MT bolum mismatch", "nUrtMTBolumID", req.NUrtMTBolumID, "ignored_count", len(ignored))
}
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "id": id, "ignored": ignored})
} }
type productionProductCostingSetActiveRequest struct { type productionProductCostingSetActiveRequest struct {

View File

@@ -0,0 +1,135 @@
/* 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", "locale", "resizeObserverGuard"],
/* =====================================================
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,
client: {
overlay: {
errors: true,
warnings: false,
runtimeErrors: false
}
},
// DEV proxy (CORS'suz)
proxy: [
{
context: ["/api"],
target: "http://localhost:8080",
changeOrigin: true,
secure: false,
ws: true,
timeout: 0,
proxyTimeout: 0
}
]
},
/* =====================================================
QUASAR FRAMEWORK
===================================================== */
framework: {
config: {
notify: {
position: "top",
timeout: 2500
}
},
lang: "tr",
plugins: [
"Loading",
"Dialog",
"Notify"
]
},
animations: [],
/* =====================================================
SSR / PWA (DISABLED)
===================================================== */
ssr: {
prodPort: 3e3,
middlewares: ["render"],
pwa: false
},
pwa: {
workboxMode: "GenerateSW"
},
/* =====================================================
MOBILE / DESKTOP
===================================================== */
capacitor: {
hideSplashscreen: true
},
electron: {
preloadScripts: ["electron-preload"],
inspectPort: 5858,
bundler: "packager",
builder: {
appId: "baggisowtfaresystem"
}
},
bex: {
extraScripts: []
}
};
});
export {
quasar_config_default as default
};

View File

@@ -44,7 +44,15 @@
@click="toggleHeaderInfo" @click="toggleHeaderInfo"
/> />
<q-btn icon="arrow_back" label="Geri" dense flat color="grey-8" class="pcd-toolbar-btn" @click="goBack" /> <q-btn icon="arrow_back" label="Geri" dense flat color="grey-8" class="pcd-toolbar-btn" @click="goBack" />
<q-btn label="Yenile" icon="refresh" dense color="primary" class="pcd-toolbar-btn" :loading="detailLoading" @click="fetchDetail" /> <q-btn
label="Yenile"
icon="refresh"
dense
color="primary"
class="pcd-toolbar-btn"
:loading="detailLoading"
@click="fetchDetail({ clearDraft: true, hydrateDraft: false })"
/>
<q-btn <q-btn
label="Toplu Fiyat Cagir" label="Toplu Fiyat Cagir"
icon="playlist_add_check" icon="playlist_add_check"
@@ -107,7 +115,6 @@
<div class="col-12 col-md-3"> <div class="col-12 col-md-3">
<q-select <q-select
v-if="!isNoCostDetail"
v-model="detailHeader.UretimSekliID" v-model="detailHeader.UretimSekliID"
:options="productionTypes" :options="productionTypes"
option-value="id" option-value="id"
@@ -120,29 +127,6 @@
class="pcd-emphasis-field-alt" class="pcd-emphasis-field-alt"
@update:model-value="onUretimSekliChange" @update:model-value="onUretimSekliChange"
/> />
<q-input
v-else
dense
filled
readonly
label="Uretim Sekli"
:model-value="detailHeader.UretimSekli || '-'"
class="pcd-emphasis-field-alt"
/>
</div>
<div class="col-12 col-md-6">
<div class="row q-col-gutter-xs">
<div class="col-4">
<q-input dense filled readonly label="USD Kuru" :model-value="formatMoney(exchangeRates.usdRate)" class="pcd-emphasis-field-alt" />
</div>
<div class="col-4">
<q-input dense filled readonly label="EUR Kuru" :model-value="formatMoney(exchangeRates.eurRate)" class="pcd-emphasis-field-alt" />
</div>
<div class="col-4">
<q-input dense filled readonly label="GBP Kuru" :model-value="formatMoney(exchangeRates.gbpRate)" class="pcd-emphasis-field-alt" />
</div>
</div>
</div> </div>
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">
@@ -160,6 +144,9 @@
<div class="col-12 col-md-3"> <div class="col-12 col-md-3">
<q-input dense filled readonly label="UrunAdi" :model-value="detailHeader.UrunAdi || '-'" /> <q-input dense filled readonly label="UrunAdi" :model-value="detailHeader.UrunAdi || '-'" />
</div> </div>
<div class="col-12 col-md-3">
<q-input dense filled readonly label="Urun Ilk Grubu" :model-value="detailHeader.UrunIlkGrubu || '-'" />
</div>
<div class="col-12 col-md-3"> <div class="col-12 col-md-3">
<q-input dense filled readonly label="Urun Ana Grubu" :model-value="detailHeader.UrunAnaGrubu || '-'" /> <q-input dense filled readonly label="Urun Ana Grubu" :model-value="detailHeader.UrunAnaGrubu || '-'" />
</div> </div>
@@ -180,9 +167,9 @@
<q-input dense filled readonly label="nUrtReceteID" :model-value="detailHeader.nUrtReceteID || '-'" /> <q-input dense filled readonly label="nUrtReceteID" :model-value="detailHeader.nUrtReceteID || '-'" />
</div> </div>
<div v-if="!isNoCostDetail && partSummary && partSummary.length > 0" class="col-12"> <div v-if="partSummary && partSummary.length > 0" class="col-12">
<div class="pcd-part-summary-card"> <div class="pcd-part-summary-card">
<div class="pcd-part-summary-title">Parça Bazlı Maliyet Özetleri</div> <div class="pcd-part-summary-title">Parça Bazlı Maliyet Özellikleri</div>
<q-markup-table dense flat bordered separator="cell" class="pcd-part-summary-table"> <q-markup-table dense flat bordered separator="cell" class="pcd-part-summary-table">
<thead> <thead>
<tr> <tr>
@@ -359,7 +346,7 @@
<template #body-cell-sParcaAdi="props"> <template #body-cell-sParcaAdi="props">
<q-td :props="props"> <q-td :props="props">
{{ props.value || props.row.sAciklama3 || '-' }} {{ props.value || '-' }}
</q-td> </q-td>
</template> </template>
@@ -826,14 +813,18 @@ const flatDetailRows = computed(() => detailGroups.value.flatMap(grp => Array.is
// no-cost: required parca slots (from Maliyet Parca Eslestirme) // no-cost: required parca slots (from Maliyet Parca Eslestirme)
const requiredParcaMappings = ref([]) const requiredParcaMappings = ref([])
const requiredAttentionRowKeys = ref({}) const requiredAttentionRowKeys = ref({})
const requiredHammaddeMetaCache = ref({}) // hammaddeNo -> { groupName, parcaAdi }
// Bump this when draft payload semantics change, to avoid hydrating stale rows after backend logic updates.
const DRAFT_STORAGE_VERSION = 'v2'
const draftStorageKey = computed(() => { const draftStorageKey = computed(() => {
if (isNoCostDetail.value) { if (isNoCostDetail.value) {
if (!recipeCode.value) return '' if (!recipeCode.value) return ''
return `pcd-costing:no-cost:${String(productCode.value || '').trim()}:${String(recipeCode.value || '').trim()}` return `pcd-costing:${DRAFT_STORAGE_VERSION}:no-cost:${String(productCode.value || '').trim()}:${String(recipeCode.value || '').trim()}`
} }
if (!onMLNo.value) return '' if (!onMLNo.value) return ''
return `pcd-costing:has-cost:${String(onMLNo.value).trim()}` return `pcd-costing:${DRAFT_STORAGE_VERSION}:has-cost:${String(onMLNo.value).trim()}`
}) })
const currentHeaderSnapshot = computed(() => JSON.stringify({ const currentHeaderSnapshot = computed(() => JSON.stringify({
@@ -1051,6 +1042,27 @@ function isCMGroupName (value) {
return normalizedValue.includes('CM1') || normalizedValue.includes('CM2') return normalizedValue.includes('CM1') || normalizedValue.includes('CM2')
} }
function isKnownGroupName (value) {
const v = String(value || '').trim().toUpperCase()
if (!v) return false
return v === 'DT' || v === 'TP' || v === 'FABRIC' || v === 'CM1' || v === 'CM2' ||
v.includes(' DT') || v.includes(' TP') || v.includes('FABRIC') || v.includes('CM1') || v.includes('CM2')
}
function normalizeLegacyParcaAndGroup (seed = {}) {
const parca = String(seed?.sParcaAdi || '').trim()
const group = String(seed?.sAciklama3 || '').trim()
if (!parca && !group) return seed
// Some legacy rows were saved with parca/group swapped. Fix it at the UI layer:
// - Parca: CEKET/PANTOLON/YELEK...
// - Group: DT/TP/CM1/CM2/FABRIC...
if (isKnownGroupName(parca) && !isKnownGroupName(group)) {
return { ...seed, sParcaAdi: group, sAciklama3: parca }
}
return seed
}
function createEmptyExchangeRates () { function createEmptyExchangeRates () {
return { return {
rateDate: '', rateDate: '',
@@ -1062,6 +1074,7 @@ function createEmptyExchangeRates () {
} }
function createRowEditorForm (seed = {}) { function createRowEditorForm (seed = {}) {
seed = normalizeLegacyParcaAndGroup(seed)
const defaultCurrency = normalizePriceCurrency(seed?.inputPricePrBr || seed?.fiyat_doviz || detailHeader.value?.sDovizCinsi) || 'USD' const defaultCurrency = normalizePriceCurrency(seed?.inputPricePrBr || seed?.fiyat_doviz || detailHeader.value?.sDovizCinsi) || 'USD'
const cmPriceTypeId = normalizeCMPriceTypeId(seed?.cmPriceTypeId ?? seed?.cm_price_type_id, seed?.sAciklama3 ?? seed?.sParcaAdi) const cmPriceTypeId = normalizeCMPriceTypeId(seed?.cmPriceTypeId ?? seed?.cm_price_type_id, seed?.sAciklama3 ?? seed?.sParcaAdi)
return { return {
@@ -1070,10 +1083,10 @@ function createRowEditorForm (seed = {}) {
nStokID: String(seed?.nStokID || '').trim(), nStokID: String(seed?.nStokID || '').trim(),
sModel: String(seed?.sModel || '').trim(), sModel: String(seed?.sModel || '').trim(),
nOnMLDetNo: String(seed?.nOnMLDetNo || '').trim(), nOnMLDetNo: String(seed?.nOnMLDetNo || '').trim(),
sParcaAdi: String(seed?.sParcaAdi || seed?.sAciklama3 || '').trim(), sParcaAdi: String(seed?.sParcaAdi || '').trim(),
nHammaddeTuruNo: String(seed?.nHammaddeTuruNo || '').trim(), nHammaddeTuruNo: String(seed?.nHammaddeTuruNo || '').trim(),
sHammaddeTuruAdi: String(seed?.sHammaddeTuruAdi || '').trim(), sHammaddeTuruAdi: String(seed?.sHammaddeTuruAdi || '').trim(),
sAciklama3: String(seed?.sAciklama3 || seed?.sParcaAdi || '').trim(), sAciklama3: String(seed?.sAciklama3 || '').trim(),
sKodu: String(seed?.sKodu || '').trim(), sKodu: String(seed?.sKodu || '').trim(),
sAciklama: String(seed?.sAciklama || '').trim(), sAciklama: String(seed?.sAciklama || '').trim(),
sRenk: String(seed?.sRenk || seed?.ColorCode || '').trim(), sRenk: String(seed?.sRenk || seed?.ColorCode || '').trim(),
@@ -1828,7 +1841,7 @@ function normalizeDetailRows (items, groupName = '') {
function normalizeDetailGroups (groups) { function normalizeDetailGroups (groups) {
const list = Array.isArray(groups) ? groups : [] const list = Array.isArray(groups) ? groups : []
return list.map(grp => { const out = list.map(grp => {
const groupName = String(grp?.sAciklama3 || '').trim() const groupName = String(grp?.sAciklama3 || '').trim()
const items = normalizeDetailRows(grp?.items, groupName).map(row => ({ const items = normalizeDetailRows(grp?.items, groupName).map(row => ({
...row, ...row,
@@ -1836,16 +1849,52 @@ function normalizeDetailGroups (groups) {
cmPriceTypeId: normalizeCMPriceTypeId(row?.cmPriceTypeId ?? row?.cm_price_type_id, groupName || row?.sAciklama3) cmPriceTypeId: normalizeCMPriceTypeId(row?.cmPriceTypeId ?? row?.cm_price_type_id, groupName || row?.sAciklama3)
})) }))
// USD TUTAR (DESC) sıralama // USD TUTAR (DESC) sıralama
if (isNoCostDetail.value) {
items.sort((a, b) => {
const ha = parseInt(String(a?.nHammaddeTuruNo || '0'), 10) || 0
const hb = parseInt(String(b?.nHammaddeTuruNo || '0'), 10) || 0
if (ha !== hb) return ha - hb
const ka = String(a?.sKodu || '').trim()
const kb = String(b?.sKodu || '').trim()
return ka.localeCompare(kb, 'tr')
})
} else {
items.sort((a, b) => { items.sort((a, b) => {
const valA = resolveRowUSDTutar(a) const valA = resolveRowUSDTutar(a)
const valB = resolveRowUSDTutar(b) const valB = resolveRowUSDTutar(b)
return valB - valA return valB - valA
}) })
}
return { return {
...grp, ...grp,
items items
} }
}) })
return sortDetailGroups(out)
}
function groupOrderIndex (name) {
const v = String(name || '').trim().toUpperCase()
if (!v) return 999
if (v.includes('CM2')) return 0
if (v.includes('FABRIC')) return 1
if (v === 'TP' || v.includes(' TP')) return 2
if (v === 'DT' || v.includes(' DT')) return 3
if (v.includes('CM1')) return 4
return 999
}
function sortDetailGroups (groups) {
const list = Array.isArray(groups) ? [...groups] : []
list.sort((a, b) => {
const ga = String(a?.sAciklama3 || '').trim()
const gb = String(b?.sAciklama3 || '').trim()
const ia = groupOrderIndex(ga)
const ib = groupOrderIndex(gb)
if (ia !== ib) return ia - ib
return ga.localeCompare(gb, 'tr')
})
return list
} }
function recalculateDetailRow (row, options = {}) { function recalculateDetailRow (row, options = {}) {
@@ -1977,11 +2026,7 @@ function resolveElHeight (refVal) {
function updateStickyTop () { function updateStickyTop () {
const stackH = resolveElHeight(stickyStackRef.value) const stackH = resolveElHeight(stickyStackRef.value)
// Quasar default header height is usually around 50px subHeaderTop.value = (stackH || 0) + 50
// If we are in a sub-layout or context where top header is not 50px, this might need adjustment
const layoutHeader = document.querySelector('.q-header')
const layoutHeaderH = layoutHeader ? layoutHeader.offsetHeight : 50
subHeaderTop.value = (stackH || 0) + layoutHeaderH
} }
function toggleHeaderInfo () { function toggleHeaderInfo () {
@@ -2052,7 +2097,9 @@ async function fetchExchangeRatesForCostDate (targetDate = costDate.value) {
} }
} }
async function fetchDetail () { async function fetchDetail (options = {}) {
const hydrateDraft = options.hydrateDraft !== false
const clearDraft = options.clearDraft === true
if (isNoCostDetail.value && !recipeCode.value) { if (isNoCostDetail.value && !recipeCode.value) {
detailError.value = 'Recete kodu bulunamadi' detailError.value = 'Recete kodu bulunamadi'
detailGroups.value = [] detailGroups.value = []
@@ -2102,6 +2149,9 @@ async function fetchDetail () {
lineHistoryLastRecipeMatchStage.value = '' lineHistoryLastRecipeMatchStage.value = ''
try { try {
if (clearDraft) {
clearLocalDraft()
}
const detailParams = buildDetailFetchParams() const detailParams = buildDetailFetchParams()
slog.info('production-product-costing.detail', 'fetch-detail:start', { slog.info('production-product-costing.detail', 'fetch-detail:start', {
trace_id: traceId.value, trace_id: traceId.value,
@@ -2123,11 +2173,13 @@ async function fetchDetail () {
detailGroups.value = normalizeDetailGroups(groupsData) detailGroups.value = normalizeDetailGroups(groupsData)
initialHeaderSnapshot.value = currentHeaderSnapshot.value initialHeaderSnapshot.value = currentHeaderSnapshot.value
// Optional: hydrate local draft after base data load. // Optional: hydrate local draft after base data load.
if (hydrateDraft) {
tryHydrateFromLocalDraft() tryHydrateFromLocalDraft()
}
// ensure required placeholder rows exist (based on mapping screen) // ensure required placeholder rows exist (based on mapping screen)
try { try {
const mappings = await fetchRequiredParcaMappings() const mappings = await fetchRequiredParcaMappings()
ensureNoCostRequiredRowsFromMappings(mappings) await ensureNoCostRequiredRowsFromMappings(mappings)
} catch (err) { } catch (err) {
slog.error('production-product-costing.detail', 'required-mapping:error', { slog.error('production-product-costing.detail', 'required-mapping:error', {
trace_id: traceId.value, trace_id: traceId.value,
@@ -2526,7 +2578,8 @@ async function bootstrapRowEditorOptions () {
if (selectedHammadde) { if (selectedHammadde) {
rowEditorForm.value.sHammaddeTuruAdi = String(selectedHammadde.sHammaddeTuruAdi || rowEditorForm.value.sHammaddeTuruAdi || '').trim() rowEditorForm.value.sHammaddeTuruAdi = String(selectedHammadde.sHammaddeTuruAdi || rowEditorForm.value.sHammaddeTuruAdi || '').trim()
rowEditorForm.value.sAciklama3 = String(selectedHammadde.sAciklama3 || rowEditorForm.value.sAciklama3 || 'TANIMSIZ').trim() || 'TANIMSIZ' rowEditorForm.value.sAciklama3 = String(selectedHammadde.sAciklama3 || rowEditorForm.value.sAciklama3 || 'TANIMSIZ').trim() || 'TANIMSIZ'
rowEditorForm.value.sParcaAdi = String(selectedHammadde.sParcaAdi || selectedHammadde.sAciklama3 || rowEditorForm.value.sParcaAdi || '').trim() // Parca adi should come from spUrtMTBolum.sAdi (backend sends as sParcaAdi). Never fall back to group label (DT/TP/...).
rowEditorForm.value.sParcaAdi = String(selectedHammadde.sParcaAdi || rowEditorForm.value.sParcaAdi || '').trim()
} }
const selectedItem = itemRows.find(opt => String(opt?.value || '') === itemSearch) const selectedItem = itemRows.find(opt => String(opt?.value || '') === itemSearch)
if (selectedItem) { if (selectedItem) {
@@ -2558,7 +2611,8 @@ function onRowEditorHammaddeChange (value) {
if (!selected) return if (!selected) return
rowEditorForm.value.sHammaddeTuruAdi = String(selected.sHammaddeTuruAdi || '').trim() rowEditorForm.value.sHammaddeTuruAdi = String(selected.sHammaddeTuruAdi || '').trim()
rowEditorForm.value.sAciklama3 = String(selected.sAciklama3 || 'TANIMSIZ').trim() || 'TANIMSIZ' rowEditorForm.value.sAciklama3 = String(selected.sAciklama3 || 'TANIMSIZ').trim() || 'TANIMSIZ'
rowEditorForm.value.sParcaAdi = String(selected.sParcaAdi || selected.sAciklama3 || '').trim() // Parca adi should come from spUrtMTBolum.sAdi (backend sends as sParcaAdi). Never fall back to group label (DT/TP/...).
rowEditorForm.value.sParcaAdi = String(selected.sParcaAdi || '').trim()
if (!isCMGroupName(rowEditorForm.value.sAciklama3)) { if (!isCMGroupName(rowEditorForm.value.sAciklama3)) {
rowEditorForm.value.cmPriceTypeChecked = false rowEditorForm.value.cmPriceTypeChecked = false
} }
@@ -2603,7 +2657,7 @@ function onRowEditorColorChange (value) {
} }
function buildRowFromEditorForm () { function buildRowFromEditorForm () {
const form = rowEditorForm.value const form = normalizeLegacyParcaAndGroup(rowEditorForm.value)
const existingRow = flatDetailRows.value.find(row => row.__rowKey === rowEditorTargetRowKey.value) const existingRow = flatDetailRows.value.find(row => row.__rowKey === rowEditorTargetRowKey.value)
const cmPriceTypeId = normalizeCMPriceTypeId(form.cmPriceTypeChecked ? 2 : 1, form.sAciklama3) const cmPriceTypeId = normalizeCMPriceTypeId(form.cmPriceTypeChecked ? 2 : 1, form.sAciklama3)
if (!existingRow) { if (!existingRow) {
@@ -2618,8 +2672,9 @@ function buildRowFromEditorForm () {
nStokID: String(form.nStokID || '').trim(), nStokID: String(form.nStokID || '').trim(),
sModel: String(form.sModel || '').trim(), sModel: String(form.sModel || '').trim(),
nOnMLDetNo: String(form.nOnMLDetNo || '').trim(), nOnMLDetNo: String(form.nOnMLDetNo || '').trim(),
sParcaAdi: String(form.sParcaAdi || form.sAciklama3 || '').trim(), // Keep Parca Adi and Parca Grubu distinct. sAciklama3 is the group key (DT/TP/CM2/FABRIC).
sAciklama3: String(form.sAciklama3 || form.sParcaAdi || 'TANIMSIZ').trim() || 'TANIMSIZ', sParcaAdi: String(form.sParcaAdi || '').trim(),
sAciklama3: String(form.sAciklama3 || 'TANIMSIZ').trim() || 'TANIMSIZ',
nHammaddeTuruNo: String(form.nHammaddeTuruNo || '').trim(), nHammaddeTuruNo: String(form.nHammaddeTuruNo || '').trim(),
sHammaddeTuruAdi: String(form.sHammaddeTuruAdi || '').trim(), sHammaddeTuruAdi: String(form.sHammaddeTuruAdi || '').trim(),
sKodu: String(form.sKodu || '').trim(), sKodu: String(form.sKodu || '').trim(),
@@ -2662,7 +2717,14 @@ function applyEditorRowToGroups (nextRow) {
nextGroups[targetIndex] = { nextGroups[targetIndex] = {
...nextGroups[targetIndex], ...nextGroups[targetIndex],
items: [...(Array.isArray(nextGroups[targetIndex].items) ? nextGroups[targetIndex].items : []), nextRow] items: [...(Array.isArray(nextGroups[targetIndex].items) ? nextGroups[targetIndex].items : []), nextRow]
.sort((left, right) => parseInt(String(left?.nOnMLDetNo || '0'), 10) - parseInt(String(right?.nOnMLDetNo || '0'), 10)) .sort((left, right) => {
if (isNoCostDetail.value) {
const ha = parseInt(String(left?.nHammaddeTuruNo || '0'), 10) || 0
const hb = parseInt(String(right?.nHammaddeTuruNo || '0'), 10) || 0
if (ha !== hb) return ha - hb
}
return parseInt(String(left?.nOnMLDetNo || '0'), 10) - parseInt(String(right?.nOnMLDetNo || '0'), 10)
})
} }
} else { } else {
nextGroups.push({ nextGroups.push({
@@ -2673,7 +2735,7 @@ function applyEditorRowToGroups (nextRow) {
}) })
} }
detailGroups.value = nextGroups detailGroups.value = sortDetailGroups(nextGroups)
syncAllGroupsOpen() syncAllGroupsOpen()
schedulePersistLocalDraft() schedulePersistLocalDraft()
} }
@@ -2695,49 +2757,143 @@ function normalizeGroupName (value) {
} }
async function fetchRequiredParcaMappings () { async function fetchRequiredParcaMappings () {
const ilk = String(detailHeader.value?.UrunIlkGrubu || '').trim()
const ana = String(detailHeader.value?.UrunAnaGrubu || '').trim() const ana = String(detailHeader.value?.UrunAnaGrubu || '').trim()
const alt = String(detailHeader.value?.UrunAltGrubu || '').trim() const alt = String(detailHeader.value?.UrunAltGrubu || '').trim()
if (!ana || !alt) return [] if (!ilk || !ana || !alt) return []
const data = await get('/pricing/production-product-costing/maliyet-parca-eslestirme', { const data = await get('/pricing/production-product-costing/maliyet-parca-eslestirme', {
trace_id: traceId.value, trace_id: traceId.value,
only_active: 1, only_active: 1,
urun_ilk_grubu: ilk,
urun_ana_grubu: ana, urun_ana_grubu: ana,
urun_alt_grubu: alt urun_alt_grubu: alt
}) })
return Array.isArray(data) ? data : [] return Array.isArray(data) ? data : []
} }
function ensureNoCostRequiredRowsFromMappings (mappings) { async function resolveHammaddeMetaForRequired (hNo) {
const key = String(hNo || '').trim()
if (!key) return null
const cached = requiredHammaddeMetaCache.value?.[key]
if (cached) return cached
try {
const rows = await get('/pricing/production-product-costing/detail-editor-options', {
kind: 'hammadde',
search: key,
limit: 25,
trace_id: traceId.value
})
const list = Array.isArray(rows) ? rows : []
const hit = list.find(x => String(x?.nHammaddeTuruNo ?? x?.value ?? '').trim() === key)
const meta = hit
? {
// In URETIM lookups, DT/TP/CM2/FABRIC is often stored in sAciklama2 (and sometimes in sAciklama3).
groupName: String(hit?.sAciklama3 || hit?.sAciklama2 || '').trim(),
// Hammadde type description is usually sAciklama (e.g. "CKT KUMAS").
// NOTE: detail-editor-options returns `sHammaddeTuruAdi` already without the number. Do not fall back to `label`,
// otherwise we end up rendering "3300 - 3300 - CKT ASKI".
hammaddeAdi: String(hit?.sHammaddeTuruAdi || hit?.sAciklama || '').trim(),
// Part info: spUrtOnMLHammaddeTuru.nUrtMTBolumID -> spUrtMTBolum.sAdi
mtBolumID: parseInt(String(hit?.mtUrtMTBolumID ?? hit?.MTUrtMTBolumID ?? hit?.nUrtMTBolumID ?? hit?.NUrtMTBolumID ?? '0'), 10) || 0,
parcaAdi: String(hit?.sParcaAdi || '').trim()
}
: null
requiredHammaddeMetaCache.value = { ...(requiredHammaddeMetaCache.value || {}), [key]: meta || { groupName: '', hammaddeAdi: '', mtBolumID: 0, parcaAdi: '' } }
return meta
} catch {
requiredHammaddeMetaCache.value = { ...(requiredHammaddeMetaCache.value || {}), [key]: { groupName: '', hammaddeAdi: '', mtBolumID: 0, parcaAdi: '' } }
return null
}
}
async function ensureNoCostRequiredRowsFromMappings (mappings) {
const list = Array.isArray(mappings) ? mappings : [] const list = Array.isArray(mappings) ? mappings : []
requiredParcaMappings.value = list requiredParcaMappings.value = list
if (list.length === 0) return if (list.length === 0) return
// Add missing placeholder rows (qty=1, price=0) to remind user // Defensive: The mapping payload may contain duplicate hammadde numbers across rows (or even inside a single row).
list.forEach(mapping => { // IMPORTANT: Placeholders are keyed by (nUrtMTBolumID + nHammaddeTuruNo). The same hammadde type can be required
const groupName = normalizeGroupName(mapping?.parcaBolumAdi || mapping?.mtBolumAdi || mapping?.sAciklama3) // for multiple parts (Ceket/Pantolon/Yelek...), so de-duping only by hNo is incorrect.
const hList = Array.isArray(mapping?.nHammaddeTurleri) ? mapping.nHammaddeTurleri : [] const processedRequiredKeys = new Set()
hList.forEach(hNoRaw => {
const hNo = normalizeHammaddeNo(hNoRaw)
if (!hNo) return
const exists = flatDetailRows.value.some(r => // Add missing placeholder rows (qty=1, price=0) to remind user
normalizeGroupName(r?.sAciklama3) === groupName && for (const mapping of list) {
normalizeHammaddeNo(r?.nHammaddeTuruNo) === hNo // Parca adi (CEKET/PANTOLON/YELEK...) comes from the MT bolum description (joined in backend as parcaBolumAdi).
) // sAciklama3 is reserved for the group header (DT/TP/CM2/FABRIC).
if (exists) return const mappingParcaAdi = normalizeGroupName(mapping?.parcaBolumAdi || mapping?.mtBolumAdi)
const mappingMtBolumID = parseInt(String(mapping?.nUrtMTBolumID ?? mapping?.NUrtMTBolumID ?? '0'), 10) || 0
const hList = Array.isArray(mapping?.nHammaddeTurleri) ? mapping.nHammaddeTurleri : []
for (const hNoRaw of hList) {
const hNo = normalizeHammaddeNo(hNoRaw)
if (!hNo) continue
const reqKey = `${mappingMtBolumID}:${hNo}`
if (processedRequiredKeys.has(reqKey)) continue
processedRequiredKeys.add(reqKey)
// Prefer the hammadde lookup's groupName (DT/TP/CM2/FABRIC)
// so placeholder rows land under the correct group headers.
const meta = await resolveHammaddeMetaForRequired(hNo)
const groupName = normalizeGroupName(meta?.groupName || '')
const hammaddeAdi = String(meta?.hammaddeAdi || '').trim()
const effectiveGroupName = groupName || 'TANIMSIZ'
const mtBolumID = (meta?.mtBolumID > 0 ? meta.mtBolumID : mappingMtBolumID) || 0
const metaParcaCandidate = normalizeGroupName((meta?.parcaAdi || '').trim())
// Defensive: if backend/lookup accidentally returns group label (DT/TP/...) as part name, ignore it.
const desiredParcaAdi = normalizeGroupName((metaParcaCandidate && !isKnownGroupName(metaParcaCandidate)) ? metaParcaCandidate : mappingParcaAdi)
const anyMatch = flatDetailRows.value.find(r => {
if (normalizeHammaddeNo(r?.nHammaddeTuruNo) !== hNo) return false
const rowMtBolumID = parseInt(String(r?.nUrtMTBolumID ?? r?.NUrtMTBolumID ?? '0'), 10) || 0
if (rowMtBolumID > 0 && mtBolumID > 0) return rowMtBolumID === mtBolumID
// If we don't have row's nUrtMTBolumID (legacy rows / older placeholders), fall back to matching by part name.
// Also accept legacy placeholders where sParcaAdi accidentally contains the group label (DT/TP/...), so we can move/fix them.
const rowParca = normalizeGroupName(r?.sParcaAdi)
return rowParca === desiredParcaAdi || isKnownGroupName(rowParca)
})
if (anyMatch) {
// If we previously created a placeholder under a wrong group name, move it instead of duplicating.
if (anyMatch?.requiredPlaceholder) {
const currentGroup = normalizeGroupName(anyMatch?.sAciklama3)
const currentParcaAdi = normalizeGroupName(anyMatch?.sParcaAdi || '')
const currentHammaddeAdi = String(anyMatch?.sHammaddeTuruAdi || '').trim()
if (currentGroup !== effectiveGroupName || currentParcaAdi !== desiredParcaAdi || (hammaddeAdi && currentHammaddeAdi !== hammaddeAdi)) {
const moved = recalculateDetailRow({
...anyMatch,
sAciklama3: effectiveGroupName,
sParcaAdi: desiredParcaAdi,
nUrtMTBolumID: mtBolumID || anyMatch?.nUrtMTBolumID || anyMatch?.NUrtMTBolumID || 0,
sHammaddeTuruAdi: hammaddeAdi || String(anyMatch?.sHammaddeTuruAdi || '').trim(),
requiredPlaceholder: true,
draftChanged: true
}, {
preserveInputs: true,
priceType: 'REQ',
updateState: 'required',
markChanged: true
})
applyEditorRowToGroups(moved)
}
}
continue
}
newRowSequence.value += 1 newRowSequence.value += 1
const rowKey = `req-auto-row-${newRowSequence.value}` const rowKey = `req-auto-row-${newRowSequence.value}`
const placeholder = recalculateDetailRow({ const placeholder = recalculateDetailRow({
__rowKey: rowKey, __rowKey: rowKey,
isNew: true, isNew: true,
nUrtMTBolumID: mtBolumID,
nOnMLNo: detailHeader.value?.nOnMLNo || onMLNo.value || '', nOnMLNo: detailHeader.value?.nOnMLNo || onMLNo.value || '',
nOnMLDetNo: '', nOnMLDetNo: '',
sParcaAdi: groupName, // Group header key
sAciklama3: groupName, sAciklama3: effectiveGroupName,
// Parca adi column value (CEKET/PANTOLON/YELEK...)
sParcaAdi: desiredParcaAdi,
nHammaddeTuruNo: hNo, nHammaddeTuruNo: hNo,
sHammaddeTuruAdi: '', // Make "Hammadde Turu" render like existing rows: "no - aciklama"
sHammaddeTuruAdi: hammaddeAdi,
sKodu: '', sKodu: '',
sAciklama: '', sAciklama: '',
sRenk: '', sRenk: '',
@@ -2765,8 +2921,8 @@ function ensureNoCostRequiredRowsFromMappings (mappings) {
}) })
applyEditorRowToGroups(placeholder) applyEditorRowToGroups(placeholder)
}) }
}) }
} }
function computeMissingRequiredSlots () { function computeMissingRequiredSlots () {
@@ -3061,6 +3217,9 @@ watch(
} }
.pcd-sticky-stack { .pcd-sticky-stack {
position: sticky;
top: 50px;
z-index: 1000;
background: #fff; background: #fff;
margin-bottom: 0; margin-bottom: 0;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
@@ -3253,6 +3412,9 @@ watch(
} }
.pcd-sub-header { .pcd-sub-header {
position: sticky !important;
top: var(--pcd-subheader-top) !important;
z-index: 990 !important;
display: flex !important; display: flex !important;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -3296,6 +3458,9 @@ watch(
} }
.pcd-detail-table :deep(.q-table thead tr:first-child th) { .pcd-detail-table :deep(.q-table thead tr:first-child th) {
position: sticky !important;
top: calc(var(--pcd-subheader-top) + 42px) !important;
z-index: 980 !important;
background: #f8f9fa !important; background: #f8f9fa !important;
opacity: 1 !important; opacity: 1 !important;
} }

View File

@@ -1,6 +1,6 @@
<template> <template>
<q-page v-if="canReadOrder" class="pcmm-page q-pa-md"> <q-page v-if="canReadOrder" class="pcmm-page q-pa-md">
<div class="pcmm-top"> <div class="pcmm-top sticky-top">
<div class="pcmm-header row items-center q-col-gutter-md"> <div class="pcmm-header row items-center q-col-gutter-md">
<div class="col"> <div class="col">
<div class="text-h6">Maliyet Parca Eslestirme</div> <div class="text-h6">Maliyet Parca Eslestirme</div>
@@ -15,12 +15,12 @@
icon="refresh" icon="refresh"
label="Yenile" label="Yenile"
:loading="loading" :loading="loading"
@click="refreshAll" @click="hardResetAndRefresh"
/> />
</div> </div>
</div> </div>
<q-separator class="q-my-md" /> <q-separator class="q-my-sm" />
</div> </div>
<div class="pcmm-table-wrap"> <div class="pcmm-table-wrap">
@@ -37,6 +37,7 @@
no-data-label="Kayit bulunamadi" no-data-label="Kayit bulunamadi"
:rows-per-page-options="[0]" :rows-per-page-options="[0]"
hide-bottom hide-bottom
sticky-header
> >
<template #header-cell="props"> <template #header-cell="props">
<q-th :props="props"> <q-th :props="props">
@@ -239,8 +240,9 @@
map-options map-options
class="pcmm-multi-select" class="pcmm-multi-select"
behavior="menu" behavior="menu"
:disable="(bolumByKey[props.row.__key] || []).length === 0"
@filter="onFilterHammadde" @filter="onFilterHammadde"
@update:model-value="(val) => { updateHammaddeSelection(props.row.__key, val); markDirty(props.row) }" @update:model-value="(val) => { updateHammaddeSelection(props.row.__key, pruneHammaddeSelection(props.row.__key, val)); markDirty(props.row) }"
style="min-width: 320px" style="min-width: 320px"
> >
<template #before-options> <template #before-options>
@@ -265,10 +267,11 @@
</template> </template>
<template #option="scope"> <template #option="scope">
<q-item v-bind="scope.itemProps"> <q-item v-bind="scope.itemProps" :disable="isHammaddeOptionDisabled(props.row.__key, scope.opt)">
<q-item-section avatar> <q-item-section avatar>
<q-checkbox <q-checkbox
:model-value="scope.selected" :model-value="scope.selected"
:disable="isHammaddeOptionDisabled(props.row.__key, scope.opt)"
tabindex="-1" tabindex="-1"
@update:model-value="() => scope.toggleOption(scope.opt)" @update:model-value="() => scope.toggleOption(scope.opt)"
@click.stop @click.stop
@@ -294,7 +297,7 @@
</template> </template>
<script setup> <script setup>
import { computed, onMounted, reactive, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { useQuasar } from 'quasar' import { useQuasar } from 'quasar'
import { get, post, del, extractApiErrorDetail } from 'src/services/api' import { get, post, del, extractApiErrorDetail } from 'src/services/api'
import { usePermission } from 'src/composables/usePermission' import { usePermission } from 'src/composables/usePermission'
@@ -305,6 +308,8 @@ const { canRead } = usePermission()
const canReadOrder = canRead('order') const canReadOrder = canRead('order')
const traceId = `pcd-mtbolum-map-${crypto?.randomUUID?.() || String(Date.now())}` const traceId = `pcd-mtbolum-map-${crypto?.randomUUID?.() || String(Date.now())}`
const prevBodyOverflow = ref(null)
const COLUMN_FILTERS_KEY = 'pcmm.mtbolum.columnFilters.v1'
const loading = ref(false) const loading = ref(false)
const saving = ref(false) const saving = ref(false)
@@ -361,6 +366,29 @@ function normalizeSearch (value) {
const columnFilters = reactive({}) const columnFilters = reactive({})
function loadSavedColumnFilters () {
try {
const raw = localStorage.getItem(COLUMN_FILTERS_KEY)
if (!raw) return
const parsed = JSON.parse(raw)
if (!parsed || typeof parsed !== 'object') return
// Replace-in-place for Vue reactivity.
for (const k of Object.keys(columnFilters)) delete columnFilters[k]
for (const [k, v] of Object.entries(parsed)) {
const text = String(v?.text ?? '')
const selected = Array.isArray(v?.selected) ? v.selected.map(x => String(x ?? '')) : []
columnFilters[k] = { text, selected }
}
} catch {}
}
function persistColumnFilters () {
try {
localStorage.setItem(COLUMN_FILTERS_KEY, JSON.stringify(columnFilters))
} catch {}
}
function getColumnFilter (name) { function getColumnFilter (name) {
if (!columnFilters[name]) { if (!columnFilters[name]) {
columnFilters[name] = { text: '', selected: [] } columnFilters[name] = { text: '', selected: [] }
@@ -377,6 +405,7 @@ function clearColumnFilter (name) {
const cf = getColumnFilter(name) const cf = getColumnFilter(name)
cf.text = '' cf.text = ''
cf.selected = [] cf.selected = []
persistColumnFilters()
} }
function clearAllColumnFilters () { function clearAllColumnFilters () {
@@ -384,6 +413,7 @@ function clearAllColumnFilters () {
if (col.name === 'copy_select' || col.name === 'save_select') continue if (col.name === 'copy_select' || col.name === 'save_select') continue
clearColumnFilter(col.name) clearColumnFilter(col.name)
} }
persistColumnFilters()
} }
function getColumnComparableValue (row, colName) { function getColumnComparableValue (row, colName) {
@@ -429,6 +459,15 @@ const rows = computed(() => {
return result return result
}) })
function hardResetAndRefresh () {
// reset view state (filters + selections + dirty)
clearAllColumnFilters()
copySelectedKeys.value = []
saveSelectedKeyMap.value = {}
clearDirty()
refreshAll()
}
function markDirty (row) { function markDirty (row) {
const key = String(row?.__key || '').trim() const key = String(row?.__key || '').trim()
if (!key) return if (!key) return
@@ -532,6 +571,7 @@ function selectAllHammadde (rowKey) {
const key = String(rowKey || '').trim() const key = String(rowKey || '').trim()
if (!key) return if (!key) return
const all = (Array.isArray(hammaddeOptions.value) ? hammaddeOptions.value : []) const all = (Array.isArray(hammaddeOptions.value) ? hammaddeOptions.value : [])
.filter(opt => !isHammaddeOptionDisabled(key, opt))
.map(o => Number(o?.value)) .map(o => Number(o?.value))
.filter(n => Number.isFinite(n) && n > 0) .filter(n => Number.isFinite(n) && n > 0)
updateHammaddeSelection(key, all) updateHammaddeSelection(key, all)
@@ -575,6 +615,12 @@ function updateBolumSelection (key, newValue) {
...(bolumByKey.value || {}), ...(bolumByKey.value || {}),
[k]: normalizeIntList(newValue) [k]: normalizeIntList(newValue)
} }
// When part selection changes, prune hammadde selection to what is valid for the selected parts.
const currentHam = hammaddeByKey.value?.[k] || []
const nextHam = pruneHammaddeSelection(k, currentHam)
if (String(nextHam) !== String(currentHam)) {
hammaddeByKey.value = { ...(hammaddeByKey.value || {}), [k]: nextHam }
}
} }
function updateHammaddeSelection (key, newValue) { function updateHammaddeSelection (key, newValue) {
@@ -586,6 +632,27 @@ function updateHammaddeSelection (key, newValue) {
} }
} }
function isHammaddeOptionDisabled (rowKey, opt) {
const mtIds = normalizeIntList(bolumByKey.value?.[rowKey] || [])
if (mtIds.length === 0) return true
const mtBolumID = Number(opt?.mtBolumID || 0)
// If lookup doesn't specify a part, allow everywhere (mtBolumID=0).
if (!Number.isFinite(mtBolumID) || mtBolumID <= 0) return false
return !mtIds.includes(mtBolumID)
}
function pruneHammaddeSelection (rowKey, list) {
const values = normalizeIntList(list || [])
if (values.length === 0) return []
const allowed = values.filter(v => {
const opt = (Array.isArray(hammaddeOptions.value) ? hammaddeOptions.value : []).find(o => Number(o?.value) === Number(v))
// If option is not currently in the option list (search/paging), keep it; backend will still validate on save.
if (!opt) return true
return !isHammaddeOptionDisabled(rowKey, opt)
})
return allowed
}
// label resolution now handled by options' `label` field + selected-item slot (see UserDetail.vue "Piyasalar"). // label resolution now handled by options' `label` field + selected-item slot (see UserDetail.vue "Piyasalar").
async function fetchMappings () { async function fetchMappings () {
loading.value = true loading.value = true
@@ -736,6 +803,8 @@ async function fetchHammaddeOptions (search) {
hammaddeOptions.value = Array.isArray(data) hammaddeOptions.value = Array.isArray(data)
? data.map(x => ({ ? data.map(x => ({
value: Number(String(x?.nHammaddeTuruNo ?? x?.value ?? '').trim()), value: Number(String(x?.nHammaddeTuruNo ?? x?.value ?? '').trim()),
// Part ownership: spUrtOnMLHammaddeTuru.MTnUrtMTBolumID (backend exposes as mtUrtMTBolumID / mtUrtMTBolumID).
mtBolumID: Number(x?.mtUrtMTBolumID ?? x?.MTUrtMTBolumID ?? 0),
label: (() => { label: (() => {
const v = Number(String(x?.nHammaddeTuruNo ?? x?.value ?? '').trim()) const v = Number(String(x?.nHammaddeTuruNo ?? x?.value ?? '').trim())
const name = String(x?.sHammaddeTuruAdi || x?.label || '').trim() const name = String(x?.sHammaddeTuruAdi || x?.label || '').trim()
@@ -833,8 +902,6 @@ async function saveKeys (keys) {
clearDirty() clearDirty()
// after saving, clear save selection to avoid accidental re-save // after saving, clear save selection to avoid accidental re-save
saveSelectedKeyMap.value = {} saveSelectedKeyMap.value = {}
// after saving, also clear column filters to avoid carrying search context
clearAllColumnFilters()
await refreshAll() await refreshAll()
} catch (e) { } catch (e) {
const detail = await extractApiErrorDetail(e) const detail = await extractApiErrorDetail(e)
@@ -845,6 +912,25 @@ async function saveKeys (keys) {
} }
onMounted(async () => { onMounted(async () => {
// This page is designed to scroll only inside the q-table body.
// Prevent global/body scrolling while the page is mounted.
try {
prevBodyOverflow.value = document?.body?.style?.overflow ?? ''
document.body.style.overflow = 'hidden'
} catch {}
loadSavedColumnFilters()
// Persist on changes (typing/selecting) with a small debounce.
let persistTimer = null
watch(
() => columnFilters,
() => {
if (persistTimer) clearTimeout(persistTimer)
persistTimer = setTimeout(() => persistColumnFilters(), 250)
},
{ deep: true }
)
try { try {
await Promise.all([ await Promise.all([
fetchMTBolumOptions(''), fetchMTBolumOptions(''),
@@ -861,11 +947,33 @@ onMounted(async () => {
}) })
} }
}) })
onBeforeUnmount(() => {
try {
if (prevBodyOverflow.value !== null) {
document.body.style.overflow = prevBodyOverflow.value || ''
}
} catch {}
})
</script> </script>
<style scoped> <style scoped>
.pcmm-page { .pcmm-page {
background: #fafafa; background: #fafafa;
display: flex;
flex-direction: column;
/* Constrain to viewport so the table body can scroll. Quasar header is 56px in this app. */
height: calc(100vh - 56px);
min-height: 0;
overflow: hidden;
/* Small safe gap under q-header */
padding-top: 10px;
}
.sticky-top {
flex: 0 0 auto;
z-index: 10;
background: #fafafa;
} }
.pcmm-header { .pcmm-header {
@@ -875,10 +983,16 @@ onMounted(async () => {
.pcmm-top { .pcmm-top {
flex: 0 0 auto; flex: 0 0 auto;
padding-bottom: 0;
margin-top: 0;
} }
.pcmm-table-wrap { .pcmm-table-wrap {
flex: 1 1 auto; flex: 1 1 auto;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
} }
.pcmm-form { .pcmm-form {
@@ -887,8 +1001,30 @@ onMounted(async () => {
} }
.pcmm-table { .pcmm-table {
max-width: 1200px; width: 100%;
max-width: 1400px;
margin: 0 auto; margin: 0 auto;
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
}
.pcmm-table :deep(.q-table__container) {
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-height: 0;
}
.pcmm-table :deep(.q-table__middle) {
flex: 1 1 auto;
min-height: 0;
overflow: auto !important;
}
.pcmm-table :deep(.q-table) {
table-layout: auto;
} }
/* Allow multi-select chips to wrap and grow vertically (PowerBI-like) */ /* Allow multi-select chips to wrap and grow vertically (PowerBI-like) */
@@ -909,17 +1045,35 @@ onMounted(async () => {
max-width: 100%; max-width: 100%;
} }
.pcmm-table :deep(.q-table__top) {
background: #fafafa;
position: sticky;
top: 0;
z-index: 20; /* Increased to be above thead th */
padding-top: 4px;
padding-bottom: 4px;
margin-bottom: 0;
}
.pcmm-table :deep(.q-table thead th) { .pcmm-table :deep(.q-table thead th) {
font-size: 11px; font-size: 11px;
padding: 3px 4px; padding: 3px 4px;
white-space: normal !important; white-space: normal !important;
vertical-align: top !important; vertical-align: top !important;
line-height: 1.15; line-height: 1.15;
background: #f0f0f0;
} }
/* Keep q-table top controls visible while scrolling (like sticky headers). */ /* Keep header fixed to the top of the scrollable middle area (directly under the top bar). */
.pcmm-table :deep(.q-table__top) { .pcmm-table :deep(.q-table__middle thead tr th) {
background: #fafafa; position: sticky;
top: 0;
z-index: 10;
}
/* Make sure body rows never paint over the sticky header while scrolling. */
.pcmm-table :deep(.q-table__middle tbody td) {
background-clip: padding-box;
} }
.pcmm-header-cell { .pcmm-header-cell {