Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -21,7 +21,8 @@ UI_DIR=/opt/bssapp/ui/dist
|
||||
# ===============================
|
||||
# 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
|
||||
URETIM_MSSQL_CONN=sqlserver://sa:Gil_0150@10.0.0.9:1433?database=URETIM&encrypt=disable
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
@@ -906,6 +907,12 @@ func main() {
|
||||
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")
|
||||
if len(jwtSecret) < 10 {
|
||||
log.Fatal("❌ JWT_SECRET tanımlı değil veya çok kısa (min 10 karakter)")
|
||||
|
||||
@@ -98,6 +98,7 @@ type ProductionHasCostDetailHeader struct {
|
||||
NOnMLNo string `json:"nOnMLNo"`
|
||||
UrunKodu string `json:"UrunKodu"`
|
||||
UrunAdi string `json:"UrunAdi"`
|
||||
UrunIlkGrubu string `json:"UrunIlkGrubu"`
|
||||
UrunAnaGrubu string `json:"UrunAnaGrubu"`
|
||||
UrunAltGrubu string `json:"UrunAltGrubu"`
|
||||
UretimSekliID string `json:"UretimSekliID"`
|
||||
@@ -131,6 +132,7 @@ type ProductionHasCostDetailEditorOption struct {
|
||||
NHammaddeTuruNo string `json:"nHammaddeTuruNo"`
|
||||
SHammaddeTuruAdi string `json:"sHammaddeTuruAdi"`
|
||||
SAciklama3 string `json:"sAciklama3"`
|
||||
MTUrtMTBolumID int `json:"mtUrtMTBolumID"`
|
||||
SKodu string `json:"sKodu"`
|
||||
SAciklama string `json:"sAciklama"`
|
||||
SModel string `json:"sModel"`
|
||||
|
||||
@@ -37,6 +37,34 @@ ORDER BY ProductCode;
|
||||
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) {
|
||||
search = strings.TrimSpace(search)
|
||||
if limit <= 0 {
|
||||
@@ -131,6 +159,7 @@ SELECT TOP (@p2)
|
||||
ISNULL(B.sAdi, '') AS sAdi
|
||||
FROM dbo.spUrtMTBolum B WITH (NOLOCK)
|
||||
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)
|
||||
ORDER BY B.nUrtMTBolumID
|
||||
`
|
||||
@@ -160,6 +189,7 @@ SELECT
|
||||
FROM dbo.mk_MaliyetParcaEslestirme M WITH (NOLOCK)
|
||||
LEFT JOIN dbo.spUrtMTBolum B WITH (NOLOCK)
|
||||
ON B.nUrtMTBolumID = M.nUrtMTBolumID
|
||||
AND ISNULL(B.nUrtTipiID, 0) = 1
|
||||
OUTER APPLY (
|
||||
SELECT
|
||||
STUFF((
|
||||
@@ -268,6 +298,97 @@ DECLARE @id INT;
|
||||
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 {
|
||||
user = strings.TrimSpace(user)
|
||||
activeVal := 0
|
||||
@@ -699,7 +820,8 @@ WITH RecipeMatch AS (
|
||||
),
|
||||
HammaddeTekil AS (
|
||||
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,
|
||||
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.
|
||||
@@ -709,6 +831,8 @@ HammaddeTekil AS (
|
||||
ISNULL(HT.sAciklama, '') AS sHammaddeTuruAdi,
|
||||
ISNULL(S.sBirimCinsi1, '') AS sBirim,
|
||||
ISNULL(RMik.lHMiktar, 0) AS lMiktar,
|
||||
ISNULL(HT.MTnUrtMTBolumID, 0) AS MTnUrtMTBolumID,
|
||||
ISNULL(B.sAdi, '') AS sParcaAdi,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY HT.nHammaddeTuruNo
|
||||
ORDER BY ISNULL(S.sModel, ISNULL(S.sKodu, ''))
|
||||
@@ -725,13 +849,20 @@ HammaddeTekil AS (
|
||||
SELECT TOP 1
|
||||
H.nHammaddeTuruNo,
|
||||
H.sAciklama,
|
||||
H.sAciklama3
|
||||
H.sAciklama2,
|
||||
H.sAciklama3,
|
||||
H.MTnUrtMTBolumID
|
||||
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
|
||||
CASE WHEN H.nUrtMTBolumID = RMik.nUrtMTBolumID THEN 0 ELSE 1 END,
|
||||
CASE WHEN H.MTnUrtMTBolumID = RMik.nUrtMTBolumID THEN 0 ELSE 1 END,
|
||||
H.nHammaddeTuruNo
|
||||
) 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
|
||||
)
|
||||
SELECT
|
||||
@@ -762,7 +893,7 @@ SELECT
|
||||
0.0 AS gbpTutar,
|
||||
HT.sBirim,
|
||||
HT.sHammaddeTuruAdi,
|
||||
HT.sHammaddeTuruAdi AS sParcaAdi
|
||||
HT.sParcaAdi AS sParcaAdi
|
||||
FROM HammaddeTekil HT
|
||||
WHERE HT.rn = 1
|
||||
ORDER BY
|
||||
@@ -800,15 +931,22 @@ func GetProductionHasCostDetailHammaddeTypeOptions(
|
||||
SELECT TOP (@p2)
|
||||
RTRIM(CONVERT(VARCHAR(32), ISNULL(T.nHammaddeTuruNo, 0))) AS nHammaddeTuruNo,
|
||||
ISNULL(T.sAciklama, '') AS sHammaddeTuruAdi,
|
||||
ISNULL(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), N'TANIMSIZ') AS sAciklama3
|
||||
FROM dbo.spUrtOnMLHammaddeTuru T
|
||||
COALESCE(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), NULLIF(LTRIM(RTRIM(T.sAciklama2)), ''), N'TANIMSIZ') AS sAciklama3,
|
||||
ISNULL(T.MTnUrtMTBolumID, 0) AS mtUrtMTBolumID,
|
||||
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
|
||||
AND (
|
||||
@p1 = ''
|
||||
OR RTRIM(CONVERT(VARCHAR(32), ISNULL(T.nHammaddeTuruNo, 0))) LIKE @p3
|
||||
OR ISNULL(T.sAciklama, '') LIKE @p3
|
||||
OR ISNULL(T.sAciklama2, '') LIKE @p3
|
||||
OR ISNULL(T.sAciklama3, '') LIKE @p3
|
||||
OR ISNULL(B.sAdi, '') LIKE @p3
|
||||
)
|
||||
ORDER BY
|
||||
CASE
|
||||
|
||||
@@ -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)
|
||||
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 {
|
||||
logger.Warn("product group query error", "err", err)
|
||||
} else {
|
||||
item.UrunIlkGrubu = ilk
|
||||
item.UrunAnaGrubu = ana
|
||||
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)
|
||||
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 {
|
||||
logger.Warn("product group query error", "err", err)
|
||||
} else {
|
||||
item.UrunIlkGrubu = ilk
|
||||
item.UrunAnaGrubu = ana
|
||||
item.UrunAltGrubu = alt
|
||||
}
|
||||
@@ -761,7 +763,7 @@ func GetProductionHasCostDetailEditorOptionsHandler(w http.ResponseWriter, r *ht
|
||||
list := make([]models.ProductionHasCostDetailEditorOption, 0, limit)
|
||||
for rows.Next() {
|
||||
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)
|
||||
log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] hammadde scan error: %v", err)
|
||||
continue
|
||||
@@ -769,7 +771,6 @@ func GetProductionHasCostDetailEditorOptionsHandler(w http.ResponseWriter, r *ht
|
||||
item.Kind = "hammadde"
|
||||
item.Value = item.NHammaddeTuruNo
|
||||
item.Label = strings.TrimSpace(item.NHammaddeTuruNo + " - " + item.SHammaddeTuruAdi)
|
||||
item.SParcaAdi = item.SAciklama3
|
||||
list = append(list, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
@@ -1958,7 +1959,18 @@ func PostProductionProductCostingParcaMappingUpsertHandler(w http.ResponseWriter
|
||||
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 {
|
||||
logger.Error("exec error", "err", err)
|
||||
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)
|
||||
_ = 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 {
|
||||
|
||||
135
ui/quasar.config.js.temporary.compiled.1778713117975.mjs
Normal file
135
ui/quasar.config.js.temporary.compiled.1778713117975.mjs
Normal 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
|
||||
};
|
||||
@@ -44,7 +44,15 @@
|
||||
@click="toggleHeaderInfo"
|
||||
/>
|
||||
<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
|
||||
label="Toplu Fiyat Cagir"
|
||||
icon="playlist_add_check"
|
||||
@@ -107,7 +115,6 @@
|
||||
|
||||
<div class="col-12 col-md-3">
|
||||
<q-select
|
||||
v-if="!isNoCostDetail"
|
||||
v-model="detailHeader.UretimSekliID"
|
||||
:options="productionTypes"
|
||||
option-value="id"
|
||||
@@ -120,29 +127,6 @@
|
||||
class="pcd-emphasis-field-alt"
|
||||
@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 class="col-12 col-md-4">
|
||||
@@ -160,6 +144,9 @@
|
||||
<div class="col-12 col-md-3">
|
||||
<q-input dense filled readonly label="UrunAdi" :model-value="detailHeader.UrunAdi || '-'" />
|
||||
</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">
|
||||
<q-input dense filled readonly label="Urun Ana Grubu" :model-value="detailHeader.UrunAnaGrubu || '-'" />
|
||||
</div>
|
||||
@@ -180,9 +167,9 @@
|
||||
<q-input dense filled readonly label="nUrtReceteID" :model-value="detailHeader.nUrtReceteID || '-'" />
|
||||
</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-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">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -359,7 +346,7 @@
|
||||
|
||||
<template #body-cell-sParcaAdi="props">
|
||||
<q-td :props="props">
|
||||
{{ props.value || props.row.sAciklama3 || '-' }}
|
||||
{{ props.value || '-' }}
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
@@ -826,14 +813,18 @@ const flatDetailRows = computed(() => detailGroups.value.flatMap(grp => Array.is
|
||||
// no-cost: required parca slots (from Maliyet Parca Eslestirme)
|
||||
const requiredParcaMappings = 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(() => {
|
||||
if (isNoCostDetail.value) {
|
||||
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 ''
|
||||
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({
|
||||
@@ -1051,6 +1042,27 @@ function isCMGroupName (value) {
|
||||
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 () {
|
||||
return {
|
||||
rateDate: '',
|
||||
@@ -1062,6 +1074,7 @@ function createEmptyExchangeRates () {
|
||||
}
|
||||
|
||||
function createRowEditorForm (seed = {}) {
|
||||
seed = normalizeLegacyParcaAndGroup(seed)
|
||||
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)
|
||||
return {
|
||||
@@ -1070,10 +1083,10 @@ function createRowEditorForm (seed = {}) {
|
||||
nStokID: String(seed?.nStokID || '').trim(),
|
||||
sModel: String(seed?.sModel || '').trim(),
|
||||
nOnMLDetNo: String(seed?.nOnMLDetNo || '').trim(),
|
||||
sParcaAdi: String(seed?.sParcaAdi || seed?.sAciklama3 || '').trim(),
|
||||
sParcaAdi: String(seed?.sParcaAdi || '').trim(),
|
||||
nHammaddeTuruNo: String(seed?.nHammaddeTuruNo || '').trim(),
|
||||
sHammaddeTuruAdi: String(seed?.sHammaddeTuruAdi || '').trim(),
|
||||
sAciklama3: String(seed?.sAciklama3 || seed?.sParcaAdi || '').trim(),
|
||||
sAciklama3: String(seed?.sAciklama3 || '').trim(),
|
||||
sKodu: String(seed?.sKodu || '').trim(),
|
||||
sAciklama: String(seed?.sAciklama || '').trim(),
|
||||
sRenk: String(seed?.sRenk || seed?.ColorCode || '').trim(),
|
||||
@@ -1828,7 +1841,7 @@ function normalizeDetailRows (items, groupName = '') {
|
||||
|
||||
function normalizeDetailGroups (groups) {
|
||||
const list = Array.isArray(groups) ? groups : []
|
||||
return list.map(grp => {
|
||||
const out = list.map(grp => {
|
||||
const groupName = String(grp?.sAciklama3 || '').trim()
|
||||
const items = normalizeDetailRows(grp?.items, groupName).map(row => ({
|
||||
...row,
|
||||
@@ -1836,16 +1849,52 @@ function normalizeDetailGroups (groups) {
|
||||
cmPriceTypeId: normalizeCMPriceTypeId(row?.cmPriceTypeId ?? row?.cm_price_type_id, groupName || row?.sAciklama3)
|
||||
}))
|
||||
// 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) => {
|
||||
const valA = resolveRowUSDTutar(a)
|
||||
const valB = resolveRowUSDTutar(b)
|
||||
return valB - valA
|
||||
})
|
||||
}
|
||||
return {
|
||||
...grp,
|
||||
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 = {}) {
|
||||
@@ -1977,11 +2026,7 @@ function resolveElHeight (refVal) {
|
||||
|
||||
function updateStickyTop () {
|
||||
const stackH = resolveElHeight(stickyStackRef.value)
|
||||
// Quasar default header height is usually around 50px
|
||||
// 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
|
||||
subHeaderTop.value = (stackH || 0) + 50
|
||||
}
|
||||
|
||||
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) {
|
||||
detailError.value = 'Recete kodu bulunamadi'
|
||||
detailGroups.value = []
|
||||
@@ -2102,6 +2149,9 @@ async function fetchDetail () {
|
||||
lineHistoryLastRecipeMatchStage.value = ''
|
||||
|
||||
try {
|
||||
if (clearDraft) {
|
||||
clearLocalDraft()
|
||||
}
|
||||
const detailParams = buildDetailFetchParams()
|
||||
slog.info('production-product-costing.detail', 'fetch-detail:start', {
|
||||
trace_id: traceId.value,
|
||||
@@ -2123,11 +2173,13 @@ async function fetchDetail () {
|
||||
detailGroups.value = normalizeDetailGroups(groupsData)
|
||||
initialHeaderSnapshot.value = currentHeaderSnapshot.value
|
||||
// Optional: hydrate local draft after base data load.
|
||||
if (hydrateDraft) {
|
||||
tryHydrateFromLocalDraft()
|
||||
}
|
||||
// ensure required placeholder rows exist (based on mapping screen)
|
||||
try {
|
||||
const mappings = await fetchRequiredParcaMappings()
|
||||
ensureNoCostRequiredRowsFromMappings(mappings)
|
||||
await ensureNoCostRequiredRowsFromMappings(mappings)
|
||||
} catch (err) {
|
||||
slog.error('production-product-costing.detail', 'required-mapping:error', {
|
||||
trace_id: traceId.value,
|
||||
@@ -2526,7 +2578,8 @@ async function bootstrapRowEditorOptions () {
|
||||
if (selectedHammadde) {
|
||||
rowEditorForm.value.sHammaddeTuruAdi = String(selectedHammadde.sHammaddeTuruAdi || rowEditorForm.value.sHammaddeTuruAdi || '').trim()
|
||||
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)
|
||||
if (selectedItem) {
|
||||
@@ -2558,7 +2611,8 @@ function onRowEditorHammaddeChange (value) {
|
||||
if (!selected) return
|
||||
rowEditorForm.value.sHammaddeTuruAdi = String(selected.sHammaddeTuruAdi || '').trim()
|
||||
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)) {
|
||||
rowEditorForm.value.cmPriceTypeChecked = false
|
||||
}
|
||||
@@ -2603,7 +2657,7 @@ function onRowEditorColorChange (value) {
|
||||
}
|
||||
|
||||
function buildRowFromEditorForm () {
|
||||
const form = rowEditorForm.value
|
||||
const form = normalizeLegacyParcaAndGroup(rowEditorForm.value)
|
||||
const existingRow = flatDetailRows.value.find(row => row.__rowKey === rowEditorTargetRowKey.value)
|
||||
const cmPriceTypeId = normalizeCMPriceTypeId(form.cmPriceTypeChecked ? 2 : 1, form.sAciklama3)
|
||||
if (!existingRow) {
|
||||
@@ -2618,8 +2672,9 @@ function buildRowFromEditorForm () {
|
||||
nStokID: String(form.nStokID || '').trim(),
|
||||
sModel: String(form.sModel || '').trim(),
|
||||
nOnMLDetNo: String(form.nOnMLDetNo || '').trim(),
|
||||
sParcaAdi: String(form.sParcaAdi || form.sAciklama3 || '').trim(),
|
||||
sAciklama3: String(form.sAciklama3 || form.sParcaAdi || 'TANIMSIZ').trim() || 'TANIMSIZ',
|
||||
// Keep Parca Adi and Parca Grubu distinct. sAciklama3 is the group key (DT/TP/CM2/FABRIC).
|
||||
sParcaAdi: String(form.sParcaAdi || '').trim(),
|
||||
sAciklama3: String(form.sAciklama3 || 'TANIMSIZ').trim() || 'TANIMSIZ',
|
||||
nHammaddeTuruNo: String(form.nHammaddeTuruNo || '').trim(),
|
||||
sHammaddeTuruAdi: String(form.sHammaddeTuruAdi || '').trim(),
|
||||
sKodu: String(form.sKodu || '').trim(),
|
||||
@@ -2662,7 +2717,14 @@ function applyEditorRowToGroups (nextRow) {
|
||||
nextGroups[targetIndex] = {
|
||||
...nextGroups[targetIndex],
|
||||
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 {
|
||||
nextGroups.push({
|
||||
@@ -2673,7 +2735,7 @@ function applyEditorRowToGroups (nextRow) {
|
||||
})
|
||||
}
|
||||
|
||||
detailGroups.value = nextGroups
|
||||
detailGroups.value = sortDetailGroups(nextGroups)
|
||||
syncAllGroupsOpen()
|
||||
schedulePersistLocalDraft()
|
||||
}
|
||||
@@ -2695,49 +2757,143 @@ function normalizeGroupName (value) {
|
||||
}
|
||||
|
||||
async function fetchRequiredParcaMappings () {
|
||||
const ilk = String(detailHeader.value?.UrunIlkGrubu || '').trim()
|
||||
const ana = String(detailHeader.value?.UrunAnaGrubu || '').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', {
|
||||
trace_id: traceId.value,
|
||||
only_active: 1,
|
||||
urun_ilk_grubu: ilk,
|
||||
urun_ana_grubu: ana,
|
||||
urun_alt_grubu: alt
|
||||
})
|
||||
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 : []
|
||||
requiredParcaMappings.value = list
|
||||
if (list.length === 0) return
|
||||
|
||||
// Add missing placeholder rows (qty=1, price=0) to remind user
|
||||
list.forEach(mapping => {
|
||||
const groupName = normalizeGroupName(mapping?.parcaBolumAdi || mapping?.mtBolumAdi || mapping?.sAciklama3)
|
||||
const hList = Array.isArray(mapping?.nHammaddeTurleri) ? mapping.nHammaddeTurleri : []
|
||||
hList.forEach(hNoRaw => {
|
||||
const hNo = normalizeHammaddeNo(hNoRaw)
|
||||
if (!hNo) return
|
||||
// Defensive: The mapping payload may contain duplicate hammadde numbers across rows (or even inside a single row).
|
||||
// IMPORTANT: Placeholders are keyed by (nUrtMTBolumID + nHammaddeTuruNo). The same hammadde type can be required
|
||||
// for multiple parts (Ceket/Pantolon/Yelek...), so de-duping only by hNo is incorrect.
|
||||
const processedRequiredKeys = new Set()
|
||||
|
||||
const exists = flatDetailRows.value.some(r =>
|
||||
normalizeGroupName(r?.sAciklama3) === groupName &&
|
||||
normalizeHammaddeNo(r?.nHammaddeTuruNo) === hNo
|
||||
)
|
||||
if (exists) return
|
||||
// Add missing placeholder rows (qty=1, price=0) to remind user
|
||||
for (const mapping of list) {
|
||||
// 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).
|
||||
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
|
||||
const rowKey = `req-auto-row-${newRowSequence.value}`
|
||||
const placeholder = recalculateDetailRow({
|
||||
__rowKey: rowKey,
|
||||
isNew: true,
|
||||
nUrtMTBolumID: mtBolumID,
|
||||
nOnMLNo: detailHeader.value?.nOnMLNo || onMLNo.value || '',
|
||||
nOnMLDetNo: '',
|
||||
sParcaAdi: groupName,
|
||||
sAciklama3: groupName,
|
||||
// Group header key
|
||||
sAciklama3: effectiveGroupName,
|
||||
// Parca adi column value (CEKET/PANTOLON/YELEK...)
|
||||
sParcaAdi: desiredParcaAdi,
|
||||
nHammaddeTuruNo: hNo,
|
||||
sHammaddeTuruAdi: '',
|
||||
// Make "Hammadde Turu" render like existing rows: "no - aciklama"
|
||||
sHammaddeTuruAdi: hammaddeAdi,
|
||||
sKodu: '',
|
||||
sAciklama: '',
|
||||
sRenk: '',
|
||||
@@ -2765,8 +2921,8 @@ function ensureNoCostRequiredRowsFromMappings (mappings) {
|
||||
})
|
||||
|
||||
applyEditorRowToGroups(placeholder)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function computeMissingRequiredSlots () {
|
||||
@@ -3061,6 +3217,9 @@ watch(
|
||||
}
|
||||
|
||||
.pcd-sticky-stack {
|
||||
position: sticky;
|
||||
top: 50px;
|
||||
z-index: 1000;
|
||||
background: #fff;
|
||||
margin-bottom: 0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
@@ -3253,6 +3412,9 @@ watch(
|
||||
}
|
||||
|
||||
.pcd-sub-header {
|
||||
position: sticky !important;
|
||||
top: var(--pcd-subheader-top) !important;
|
||||
z-index: 990 !important;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -3296,6 +3458,9 @@ watch(
|
||||
}
|
||||
|
||||
.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;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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="col">
|
||||
<div class="text-h6">Maliyet Parca Eslestirme</div>
|
||||
@@ -15,12 +15,12 @@
|
||||
icon="refresh"
|
||||
label="Yenile"
|
||||
:loading="loading"
|
||||
@click="refreshAll"
|
||||
@click="hardResetAndRefresh"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator class="q-my-md" />
|
||||
<q-separator class="q-my-sm" />
|
||||
</div>
|
||||
|
||||
<div class="pcmm-table-wrap">
|
||||
@@ -37,6 +37,7 @@
|
||||
no-data-label="Kayit bulunamadi"
|
||||
:rows-per-page-options="[0]"
|
||||
hide-bottom
|
||||
sticky-header
|
||||
>
|
||||
<template #header-cell="props">
|
||||
<q-th :props="props">
|
||||
@@ -239,8 +240,9 @@
|
||||
map-options
|
||||
class="pcmm-multi-select"
|
||||
behavior="menu"
|
||||
:disable="(bolumByKey[props.row.__key] || []).length === 0"
|
||||
@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"
|
||||
>
|
||||
<template #before-options>
|
||||
@@ -265,10 +267,11 @@
|
||||
</template>
|
||||
|
||||
<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-checkbox
|
||||
:model-value="scope.selected"
|
||||
:disable="isHammaddeOptionDisabled(props.row.__key, scope.opt)"
|
||||
tabindex="-1"
|
||||
@update:model-value="() => scope.toggleOption(scope.opt)"
|
||||
@click.stop
|
||||
@@ -294,7 +297,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { get, post, del, extractApiErrorDetail } from 'src/services/api'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
@@ -305,6 +308,8 @@ const { canRead } = usePermission()
|
||||
const canReadOrder = canRead('order')
|
||||
|
||||
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 saving = ref(false)
|
||||
@@ -361,6 +366,29 @@ function normalizeSearch (value) {
|
||||
|
||||
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) {
|
||||
if (!columnFilters[name]) {
|
||||
columnFilters[name] = { text: '', selected: [] }
|
||||
@@ -377,6 +405,7 @@ function clearColumnFilter (name) {
|
||||
const cf = getColumnFilter(name)
|
||||
cf.text = ''
|
||||
cf.selected = []
|
||||
persistColumnFilters()
|
||||
}
|
||||
|
||||
function clearAllColumnFilters () {
|
||||
@@ -384,6 +413,7 @@ function clearAllColumnFilters () {
|
||||
if (col.name === 'copy_select' || col.name === 'save_select') continue
|
||||
clearColumnFilter(col.name)
|
||||
}
|
||||
persistColumnFilters()
|
||||
}
|
||||
|
||||
function getColumnComparableValue (row, colName) {
|
||||
@@ -429,6 +459,15 @@ const rows = computed(() => {
|
||||
return result
|
||||
})
|
||||
|
||||
function hardResetAndRefresh () {
|
||||
// reset view state (filters + selections + dirty)
|
||||
clearAllColumnFilters()
|
||||
copySelectedKeys.value = []
|
||||
saveSelectedKeyMap.value = {}
|
||||
clearDirty()
|
||||
refreshAll()
|
||||
}
|
||||
|
||||
function markDirty (row) {
|
||||
const key = String(row?.__key || '').trim()
|
||||
if (!key) return
|
||||
@@ -532,6 +571,7 @@ function selectAllHammadde (rowKey) {
|
||||
const key = String(rowKey || '').trim()
|
||||
if (!key) return
|
||||
const all = (Array.isArray(hammaddeOptions.value) ? hammaddeOptions.value : [])
|
||||
.filter(opt => !isHammaddeOptionDisabled(key, opt))
|
||||
.map(o => Number(o?.value))
|
||||
.filter(n => Number.isFinite(n) && n > 0)
|
||||
updateHammaddeSelection(key, all)
|
||||
@@ -575,6 +615,12 @@ function updateBolumSelection (key, newValue) {
|
||||
...(bolumByKey.value || {}),
|
||||
[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) {
|
||||
@@ -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").
|
||||
async function fetchMappings () {
|
||||
loading.value = true
|
||||
@@ -736,6 +803,8 @@ async function fetchHammaddeOptions (search) {
|
||||
hammaddeOptions.value = Array.isArray(data)
|
||||
? data.map(x => ({
|
||||
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: (() => {
|
||||
const v = Number(String(x?.nHammaddeTuruNo ?? x?.value ?? '').trim())
|
||||
const name = String(x?.sHammaddeTuruAdi || x?.label || '').trim()
|
||||
@@ -833,8 +902,6 @@ async function saveKeys (keys) {
|
||||
clearDirty()
|
||||
// after saving, clear save selection to avoid accidental re-save
|
||||
saveSelectedKeyMap.value = {}
|
||||
// after saving, also clear column filters to avoid carrying search context
|
||||
clearAllColumnFilters()
|
||||
await refreshAll()
|
||||
} catch (e) {
|
||||
const detail = await extractApiErrorDetail(e)
|
||||
@@ -845,6 +912,25 @@ async function saveKeys (keys) {
|
||||
}
|
||||
|
||||
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 {
|
||||
await Promise.all([
|
||||
fetchMTBolumOptions(''),
|
||||
@@ -861,11 +947,33 @@ onMounted(async () => {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
try {
|
||||
if (prevBodyOverflow.value !== null) {
|
||||
document.body.style.overflow = prevBodyOverflow.value || ''
|
||||
}
|
||||
} catch {}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pcmm-page {
|
||||
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 {
|
||||
@@ -875,10 +983,16 @@ onMounted(async () => {
|
||||
|
||||
.pcmm-top {
|
||||
flex: 0 0 auto;
|
||||
padding-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.pcmm-table-wrap {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pcmm-form {
|
||||
@@ -887,8 +1001,30 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.pcmm-table {
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
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) */
|
||||
@@ -909,17 +1045,35 @@ onMounted(async () => {
|
||||
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) {
|
||||
font-size: 11px;
|
||||
padding: 3px 4px;
|
||||
white-space: normal !important;
|
||||
vertical-align: top !important;
|
||||
line-height: 1.15;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
/* Keep q-table top controls visible while scrolling (like sticky headers). */
|
||||
.pcmm-table :deep(.q-table__top) {
|
||||
background: #fafafa;
|
||||
/* Keep header fixed to the top of the scrollable middle area (directly under the top bar). */
|
||||
.pcmm-table :deep(.q-table__middle thead tr th) {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user