Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-05-15 19:43:44 +03:00
parent dacd3aefa9
commit 295924cf1e
16 changed files with 1500 additions and 601 deletions

View File

@@ -796,11 +796,26 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
"order", "view", "order", "view",
wrapV3(http.HandlerFunc(routes.PostProductionHasCostDetailBulkPricesHandler)), wrapV3(http.HandlerFunc(routes.PostProductionHasCostDetailBulkPricesHandler)),
) )
bindV3(r, pgDB,
"/api/pricing/production-product-costing/has-cost-detail/last-detail", "POST",
"order", "view",
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingHasCostDetailLastDetailHandler)),
)
bindV3(r, pgDB,
"/api/pricing/production-product-costing/options/hammadde-by-nos", "POST",
"order", "view",
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingOptionsHammaddeByNosHandler)),
)
bindV3(r, pgDB, bindV3(r, pgDB,
"/api/pricing/production-product-costing/onml/save", "POST", "/api/pricing/production-product-costing/onml/save", "POST",
"order", "view", "order", "view",
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingOnMLSaveHandler)), wrapV3(http.HandlerFunc(routes.PostProductionProductCostingOnMLSaveHandler)),
) )
bindV3(r, pgDB,
"/api/pricing/production-product-costing/onml/delete", "POST",
"order", "view",
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingOnMLDeleteHandler)),
)
bindV3(r, pgDB, bindV3(r, pgDB,
"/api/pricing/production-product-costing/default-quantities", "GET", "/api/pricing/production-product-costing/default-quantities", "GET",
"order", "view", "order", "view",

View File

@@ -149,6 +149,7 @@ type ProductionProductCostingOnMLSaveDetailUpsertRow struct {
FiyatDoviz string `json:"fiyat_doviz"` FiyatDoviz string `json:"fiyat_doviz"`
MaliyeteDahil int `json:"maliyete_dahil"` MaliyeteDahil int `json:"maliyete_dahil"`
CMPriceTypeID *int `json:"cm_price_type_id"` CMPriceTypeID *int `json:"cm_price_type_id"`
SAciklama3 string `json:"s_aciklama3"`
} }
type ProductionProductCostingOnMLSaveDetailDeleteRow struct { type ProductionProductCostingOnMLSaveDetailDeleteRow struct {
@@ -170,6 +171,10 @@ type ProductionProductCostingOnMLSaveResponse struct {
NOnMLNo int `json:"n_onml_no"` NOnMLNo int `json:"n_onml_no"`
} }
type ProductionProductCostingOnMLDeleteRequest struct {
NOnMLNo int `json:"n_onml_no"`
}
// ============================================================ // ============================================================
// Default quantities (URETIM): mk_MaliyetParcaEslestirme_vmiktarlar // Default quantities (URETIM): mk_MaliyetParcaEslestirme_vmiktarlar
// ============================================================ // ============================================================
@@ -209,9 +214,39 @@ type ProductionProductCostingDefaultQtyLookupRequest struct {
type ProductionProductCostingDefaultQtyLookupItem struct { type ProductionProductCostingDefaultQtyLookupItem struct {
NHammaddeTuruNo int `json:"nHammaddeTuruNo"` NHammaddeTuruNo int `json:"nHammaddeTuruNo"`
SAciklama string `json:"sAciklama"`
LDefaultMiktar float64 `json:"lDefaultMiktar"` LDefaultMiktar float64 `json:"lDefaultMiktar"`
} }
type ProductionProductCostingLastOnMLDetLookupRequest struct {
NHammaddeTuruNos []int `json:"nHammaddeTuruNos"`
BeforeDate string `json:"before_date"` // YYYY-MM-DD (optional)
ExcludeOnMLNo int `json:"exclude_onml_no"` // optional
NFirmaID int `json:"n_firma_id"` // optional
OnlyICode bool `json:"only_i_code"` // optional: restrict to sKodu like 'I.%'
}
type ProductionProductCostingLastOnMLDetLookupItem struct {
NHammaddeTuruNo int `json:"nHammaddeTuruNo"`
SKodu string `json:"sKodu"`
SAciklama string `json:"sAciklama"`
SBirim string `json:"sBirim"`
FiyatDoviz string `json:"fiyat_doviz"`
FiyatGirilen float64 `json:"fiyat_girilen"`
IsSameFirma bool `json:"is_same_firma"`
}
type ProductionProductCostingHammaddeByNosRequest struct {
NHammaddeTuruNos []int `json:"nHammaddeTuruNos"`
}
type ProductionProductCostingHammaddeByNosItem struct {
NHammaddeTuruNo int `json:"nHammaddeTuruNo"`
SAciklama string `json:"sAciklama"`
MTUrtMTBolumID int `json:"mtUrtMTBolumID"`
SParcaAdi string `json:"sParcaAdi"`
}
type ProductionHasCostDetailExchangeRates struct { type ProductionHasCostDetailExchangeRates struct {
RateDate string `json:"rateDate"` RateDate string `json:"rateDate"`
TRYRate float64 `json:"tryRate"` TRYRate float64 `json:"tryRate"`

View File

@@ -1,6 +1,7 @@
package queries package queries
import ( import (
"bssapp-backend/models"
"bssapp-backend/utils" "bssapp-backend/utils"
"context" "context"
"database/sql" "database/sql"
@@ -227,17 +228,47 @@ func UpsertV3ItemBasePriceUSD(
return fmt.Errorf("missing params for base price upsert") return fmt.Errorf("missing params for base price upsert")
} }
// NOTE: In this DB, PRIMARY KEY is on: // 1. Find a CountryCode that is NOT yet used for this specific item in prItemBasePrice.
// (ItemTypeCode, ItemCode, CountryCode, SeasonCode, BasePriceCode) // We query cdCountry for a code that has no matching entry in prItemBasePrice for this item.
// so we cannot insert multiple rows for different dates under the same base price. // We exclude 'TR' to keep the original record safe.
// We update the single row's PriceDate/Price to reflect latest costing. var targetCountry string
err := mssqlDB.QueryRowContext(ctx, `
SELECT TOP 1 C.CountryCode
FROM dbo.cdCountry C WITH (NOLOCK)
WHERE C.CountryCode <> 'TR'
AND NOT EXISTS (
SELECT 1 FROM dbo.prItemBasePrice P WITH (NOLOCK)
WHERE P.ItemTypeCode = 1
AND LTRIM(RTRIM(P.ItemCode)) = LTRIM(RTRIM(@p1))
AND P.BasePriceCode = 1
AND P.CountryCode = C.CountryCode
)
ORDER BY NEWID() -- Randomly pick one of the available country codes
`, itemCode).Scan(&targetCountry)
if err != nil {
if err == sql.ErrNoRows {
// Fallback: If ALL countries are exhausted (unlikely), default to 'AD' as a last resort.
targetCountry = "AD"
} else {
return fmt.Errorf("failed to find available country code: %w", err)
}
}
appUser := user
if strings.TrimSpace(appUser) == "" {
appUser = "BSSAPP"
} else {
appUser = "BSSAPP:" + strings.TrimSpace(appUser)
}
sqlText := ` sqlText := `
MERGE dbo.prItemBasePrice AS T MERGE dbo.prItemBasePrice AS T
USING ( USING (
SELECT SELECT
@p1 AS ItemTypeCode, @p1 AS ItemTypeCode,
@p2 AS ItemCode, @p2 AS ItemCode,
'TR' AS CountryCode, @p6 AS CountryCode,
'' AS SeasonCode, '' AS SeasonCode,
1 AS BasePriceCode, 1 AS BasePriceCode,
CONVERT(date, @p3, 23) AS PriceDate, CONVERT(date, @p3, 23) AS PriceDate,
@@ -266,7 +297,7 @@ WHEN NOT MATCHED THEN
); );
` `
_, err := mssqlDB.ExecContext(ctx, sqlText, 1, itemCode, priceDate, priceUSD, user) _, err = mssqlDB.ExecContext(ctx, sqlText, 1, itemCode, priceDate, priceUSD, appUser, targetCountry)
return err return err
} }
@@ -1228,6 +1259,73 @@ ORDER BY
return uretimDB.QueryContext(ctx, sqlText, search, limit, searchLike) return uretimDB.QueryContext(ctx, sqlText, search, limit, searchLike)
} }
func GetProductionHammaddeByNos(ctx context.Context, uretimDB *sql.DB, nos []int) ([]models.ProductionProductCostingHammaddeByNosItem, error) {
clean := make([]int, 0, len(nos))
seen := make(map[int]struct{}, len(nos))
for _, n := range nos {
if n <= 0 {
continue
}
if _, ok := seen[n]; ok {
continue
}
seen[n] = struct{}{}
clean = append(clean, n)
}
if len(clean) == 0 {
return []models.ProductionProductCostingHammaddeByNosItem{}, nil
}
if len(clean) > 5000 {
return nil, fmt.Errorf("too many nos")
}
valueRows := make([]string, 0, len(clean))
args := make([]any, 0, len(clean))
for i, n := range clean {
valueRows = append(valueRows, "(@p"+strconv.Itoa(i+1)+")")
args = append(args, n)
}
sqlText := `
WITH req(nHammaddeTuruNo) AS (
SELECT DISTINCT v.nHammaddeTuruNo
FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(nHammaddeTuruNo)
)
SELECT
ISNULL(T.nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
ISNULL(T.sAciklama, '') AS sAciklama,
ISNULL(T.MTnUrtMTBolumID, 0) AS mtUrtMTBolumID,
ISNULL(B.sAdi, '') AS sParcaAdi
FROM dbo.spUrtOnMLHammaddeTuru T WITH (NOLOCK)
INNER JOIN req R
ON R.nHammaddeTuruNo = T.nHammaddeTuruNo
LEFT JOIN dbo.spUrtMTBolum B WITH (NOLOCK)
ON B.nUrtMTBolumID = T.MTnUrtMTBolumID
AND ISNULL(B.nUrtTipiID, 0) = 1
WHERE ISNULL(T.bAktif, 0) = 1
ORDER BY T.nHammaddeTuruNo;
`
rows, err := uretimDB.QueryContext(ctx, sqlText, args...)
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]models.ProductionProductCostingHammaddeByNosItem, 0, len(clean))
for rows.Next() {
var it models.ProductionProductCostingHammaddeByNosItem
if err := rows.Scan(&it.NHammaddeTuruNo, &it.SAciklama, &it.MTUrtMTBolumID, &it.SParcaAdi); err != nil {
return nil, err
}
out = append(out, it)
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
}
// ============================================================ // ============================================================
// Default quantities (URETIM): mk_MaliyetParcaEslestirme_vmiktarlar // Default quantities (URETIM): mk_MaliyetParcaEslestirme_vmiktarlar
// ============================================================ // ============================================================
@@ -1248,7 +1346,9 @@ SELECT TOP (@p3)
ISNULL(H.sAciklama, '') AS sAciklama, ISNULL(H.sAciklama, '') AS sAciklama,
ISNULL(V.lDefaultMiktar, 0) AS lDefaultMiktar, ISNULL(V.lDefaultMiktar, 0) AS lDefaultMiktar,
CONVERT(VARCHAR(16), V.dteCalcTarihi, 120) AS dteCalcTarihi, CONVERT(VARCHAR(16), V.dteCalcTarihi, 120) AS dteCalcTarihi,
CAST(CASE WHEN ISNULL(V.bAktif, 0) = 1 THEN 1 ELSE 0 END AS bit) AS bAktif -- We intentionally don't depend on mk_MaliyetParcaEslestirme_vmiktarlar.bAktif
-- to keep this feature robust across schema changes; only active hammadde types are listed.
CAST(1 AS bit) AS bAktif
FROM dbo.mk_MaliyetParcaEslestirme_vmiktarlar V WITH (NOLOCK) FROM dbo.mk_MaliyetParcaEslestirme_vmiktarlar V WITH (NOLOCK)
LEFT JOIN dbo.spUrtOnMLHammaddeTuru H WITH (NOLOCK) LEFT JOIN dbo.spUrtOnMLHammaddeTuru H WITH (NOLOCK)
ON H.nHammaddeTuruNo = V.nHammaddeTuruNo ON H.nHammaddeTuruNo = V.nHammaddeTuruNo
@@ -1264,26 +1364,16 @@ ORDER BY
func UpsertProductionProductCostingDefaultQtyRow(ctx context.Context, uretimDB *sql.DB, nHammaddeTuruNo int, lDefaultMiktar float64, bAktif *bool) error { func UpsertProductionProductCostingDefaultQtyRow(ctx context.Context, uretimDB *sql.DB, nHammaddeTuruNo int, lDefaultMiktar float64, bAktif *bool) error {
// NOTE: Legacy helper kept for backward-compat but now behaves as UPDATE-only. // NOTE: Legacy helper kept for backward-compat but now behaves as UPDATE-only.
activeVal := -1
if bAktif != nil {
if *bAktif {
activeVal = 1
} else {
activeVal = 0
}
}
sqlText := ` sqlText := `
UPDATE dbo.mk_MaliyetParcaEslestirme_vmiktarlar UPDATE dbo.mk_MaliyetParcaEslestirme_vmiktarlar
SET SET
lDefaultMiktar = @p2, lDefaultMiktar = @p2,
dteCalcTarihi = GETDATE(), dteCalcTarihi = GETDATE()
bAktif = CASE WHEN @p3 < 0 THEN ISNULL(bAktif, 1) ELSE @p3 END
WHERE nHammaddeTuruNo = @p1; WHERE nHammaddeTuruNo = @p1;
SELECT @@ROWCOUNT; SELECT @@ROWCOUNT;
` `
var affected int var affected int
if err := uretimDB.QueryRowContext(ctx, sqlText, nHammaddeTuruNo, lDefaultMiktar, activeVal).Scan(&affected); err != nil { if err := uretimDB.QueryRowContext(ctx, sqlText, nHammaddeTuruNo, lDefaultMiktar).Scan(&affected); err != nil {
return err return err
} }
if affected == 0 { if affected == 0 {
@@ -1333,11 +1423,10 @@ USING agg AS S
WHEN MATCHED THEN WHEN MATCHED THEN
UPDATE SET UPDATE SET
T.lDefaultMiktar = S.lDefaultMiktar, T.lDefaultMiktar = S.lDefaultMiktar,
T.dteCalcTarihi = GETDATE(), T.dteCalcTarihi = GETDATE()
T.bAktif = 1
WHEN NOT MATCHED THEN WHEN NOT MATCHED THEN
INSERT (nHammaddeTuruNo, lDefaultMiktar, dteCalcTarihi, bAktif) INSERT (nHammaddeTuruNo, lDefaultMiktar, dteCalcTarihi)
VALUES (S.nHammaddeTuruNo, S.lDefaultMiktar, GETDATE(), 1); VALUES (S.nHammaddeTuruNo, S.lDefaultMiktar, GETDATE());
` `
_, err := uretimDB.ExecContext(ctx, sqlText, topN) _, err := uretimDB.ExecContext(ctx, sqlText, topN)
return err return err
@@ -1377,7 +1466,11 @@ FROM ranked;
return avg, cnt, nil return avg, cnt, nil
} }
func LookupProductionProductCostingDefaultQtyByNos(ctx context.Context, uretimDB *sql.DB, nos []int) (map[int]float64, error) { func LookupProductionProductCostingDefaultQtyByNos(ctx context.Context, uretimDB *sql.DB, nos []int) ([]struct {
No int
Aciklama string
Qty float64
}, error) {
clean := make([]int, 0, len(nos)) clean := make([]int, 0, len(nos))
seen := make(map[int]struct{}, len(nos)) seen := make(map[int]struct{}, len(nos))
for _, n := range nos { for _, n := range nos {
@@ -1391,7 +1484,11 @@ func LookupProductionProductCostingDefaultQtyByNos(ctx context.Context, uretimDB
clean = append(clean, n) clean = append(clean, n)
} }
if len(clean) == 0 { if len(clean) == 0 {
return map[int]float64{}, nil return []struct {
No int
Aciklama string
Qty float64
}{}, nil
} }
if len(clean) > 1000 { if len(clean) > 1000 {
return nil, fmt.Errorf("too many hammadde nos") return nil, fmt.Errorf("too many hammadde nos")
@@ -1411,6 +1508,7 @@ WITH req(nHammaddeTuruNo) AS (
) )
SELECT SELECT
ISNULL(V.nHammaddeTuruNo, 0) AS nHammaddeTuruNo, ISNULL(V.nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
ISNULL(H.sAciklama, '') AS sAciklama,
CAST(ISNULL(V.lDefaultMiktar, 0) AS FLOAT) AS lDefaultMiktar CAST(ISNULL(V.lDefaultMiktar, 0) AS FLOAT) AS lDefaultMiktar
FROM req R FROM req R
INNER JOIN dbo.mk_MaliyetParcaEslestirme_vmiktarlar V WITH (NOLOCK) INNER JOIN dbo.mk_MaliyetParcaEslestirme_vmiktarlar V WITH (NOLOCK)
@@ -1426,15 +1524,24 @@ WHERE ISNULL(H.bAktif, 0) = 1;
} }
defer rows.Close() defer rows.Close()
out := make(map[int]float64, len(clean)) out := make([]struct {
No int
Aciklama string
Qty float64
}, 0, len(clean))
for rows.Next() { for rows.Next() {
var no int var no int
var aciklama sql.NullString
var qty float64 var qty float64
if err := rows.Scan(&no, &qty); err != nil { if err := rows.Scan(&no, &aciklama, &qty); err != nil {
return nil, err return nil, err
} }
if no > 0 && qty > 0 { if no > 0 && qty > 0 {
out[no] = qty out = append(out, struct {
No int
Aciklama string
Qty float64
}{No: no, Aciklama: strings.TrimSpace(aciklama.String), Qty: qty})
} }
} }
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
@@ -1443,6 +1550,128 @@ WHERE ISNULL(H.bAktif, 0) = 1;
return out, nil return out, nil
} }
func LookupLastOnMLMasDetByHammaddeNos(
ctx context.Context,
uretimDB *sql.DB,
nos []int,
beforeDate string, // YYYY-MM-DD optional
excludeOnMLNo int,
nFirmaID int,
onlyICode bool,
limitPerType int,
) ([]models.ProductionProductCostingLastOnMLDetLookupItem, error) {
if limitPerType <= 0 {
limitPerType = 1
}
if limitPerType > 1 {
limitPerType = 1
}
clean := make([]int, 0, len(nos))
seen := make(map[int]struct{}, len(nos))
for _, n := range nos {
if n <= 0 {
continue
}
if _, ok := seen[n]; ok {
continue
}
seen[n] = struct{}{}
clean = append(clean, n)
}
if len(clean) == 0 {
return []models.ProductionProductCostingLastOnMLDetLookupItem{}, nil
}
if len(clean) > 500 {
return nil, fmt.Errorf("too many hammadde nos")
}
valueRows := make([]string, 0, len(clean))
args := make([]any, 0, len(clean)+2)
for i, n := range clean {
valueRows = append(valueRows, "(@p"+strconv.Itoa(i+1)+")")
args = append(args, n)
}
// extra params at the end
args = append(args, strings.TrimSpace(beforeDate))
args = append(args, excludeOnMLNo)
args = append(args, nFirmaID)
if onlyICode {
args = append(args, 1)
} else {
args = append(args, 0)
}
beforeParam := "@p" + strconv.Itoa(len(clean)+1)
excludeParam := "@p" + strconv.Itoa(len(clean)+2)
firmaParam := "@p" + strconv.Itoa(len(clean)+3)
onlyIParam := "@p" + strconv.Itoa(len(clean)+4)
sqlText := `
WITH req(nHammaddeTuruNo) AS (
SELECT DISTINCT v.nHammaddeTuruNo
FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(nHammaddeTuruNo)
),
ranked AS (
SELECT
D.nHammaddeTuruNo,
LTRIM(RTRIM(ISNULL(D.sKodu, ''))) AS sKodu,
LTRIM(RTRIM(ISNULL(D.sAciklama, ''))) AS sAciklama,
LTRIM(RTRIM(ISNULL(D.sBirim, ''))) AS sBirim,
LTRIM(RTRIM(ISNULL(D.fiyat_doviz, ''))) AS fiyat_doviz,
CAST(ISNULL(D.fiyat_girilen, 0) AS FLOAT) AS fiyat_girilen,
CAST(CASE WHEN ISNULL(` + firmaParam + `, 0) > 0 AND ISNULL(M.nFirmaID, 0) = ISNULL(` + firmaParam + `, 0) THEN 1 ELSE 0 END AS bit) AS is_same_firma,
ROW_NUMBER() OVER (
PARTITION BY D.nHammaddeTuruNo
ORDER BY
CASE WHEN ISNULL(` + firmaParam + `, 0) > 0 AND ISNULL(M.nFirmaID, 0) = ISNULL(` + firmaParam + `, 0) THEN 0 ELSE 1 END,
COALESCE(M.Tarihi, M.dteKayitTarihi) DESC,
M.nOnMLNo DESC,
D.nOnMLDetNo DESC
) AS rn
FROM dbo.spUrtOnMLMasDet D WITH (NOLOCK)
INNER JOIN dbo.spUrtOnMLMas M WITH (NOLOCK)
ON M.nOnMLNo = D.nOnMLNo
INNER JOIN req R
ON R.nHammaddeTuruNo = D.nHammaddeTuruNo
WHERE ISNULL(D.fiyat_girilen, 0) > 0
AND LTRIM(RTRIM(ISNULL(D.sKodu, ''))) <> ''
AND (ISNULL(` + onlyIParam + `, 0) = 0 OR LTRIM(RTRIM(ISNULL(D.sKodu, ''))) LIKE 'I.%')
AND (` + beforeParam + ` = '' OR CONVERT(date, COALESCE(M.Tarihi, M.dteKayitTarihi)) < CONVERT(date, ` + beforeParam + `, 23))
AND (ISNULL(` + excludeParam + `, 0) <= 0 OR M.nOnMLNo <> ` + excludeParam + `)
)
SELECT
ISNULL(nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
ISNULL(sKodu, '') AS sKodu,
ISNULL(sAciklama, '') AS sAciklama,
ISNULL(sBirim, '') AS sBirim,
ISNULL(fiyat_doviz, '') AS fiyat_doviz,
ISNULL(fiyat_girilen, 0) AS fiyat_girilen,
CAST(ISNULL(is_same_firma, 0) AS bit) AS is_same_firma
FROM ranked
WHERE rn = 1;
`
rows, err := uretimDB.QueryContext(ctx, sqlText, args...)
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]models.ProductionProductCostingLastOnMLDetLookupItem, 0, len(clean))
for rows.Next() {
var item models.ProductionProductCostingLastOnMLDetLookupItem
if err := rows.Scan(&item.NHammaddeTuruNo, &item.SKodu, &item.SAciklama, &item.SBirim, &item.FiyatDoviz, &item.FiyatGirilen, &item.IsSameFirma); err != nil {
return nil, err
}
out = append(out, item)
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
}
func buildSQLServerFullTextPrefixQuery(search string) string { func buildSQLServerFullTextPrefixQuery(search string) string {
terms := strings.Fields(strings.TrimSpace(search)) terms := strings.Fields(strings.TrimSpace(search))
parts := make([]string, 0, len(terms)) parts := make([]string, 0, len(terms))

View File

@@ -496,6 +496,13 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc {
return return
} }
// Keep mk_mail in sync for downstream mail mapping screens.
if err := ensureMkMail(tx, payload.Email); err != nil {
log.Printf("USER CREATE ensureMkMail ERROR user_id=%d email=%q err=%v", newID, payload.Email, err)
http.Error(w, "Mail kaydi guncellenemedi", http.StatusInternalServerError)
return
}
// ROLES // ROLES
for _, role := range payload.Roles { for _, role := range payload.Roles {
role = strings.TrimSpace(role) role = strings.TrimSpace(role)

View File

@@ -0,0 +1,68 @@
package routes
import (
"bssapp-backend/utils"
"database/sql"
"errors"
"regexp"
"strings"
)
var simpleEmailRe = regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`)
// ensureMkMail makes sure the given email exists in mk_mail.
// If it already exists (case-insensitive match), it is re-activated and normalized.
// This is intentionally tolerant (no ON CONFLICT) to avoid relying on a specific unique constraint.
func ensureMkMail(tx *sql.Tx, email string) error {
mail := strings.ToLower(strings.TrimSpace(email))
if mail == "" {
return nil
}
if !simpleEmailRe.MatchString(mail) {
// user email field can be free-form; don't hard fail user save because of mk_mail bookkeeping
return nil
}
var id string
err := tx.QueryRow(`
SELECT m.id::text
FROM mk_mail m
WHERE LOWER(TRIM(m.email)) = LOWER(TRIM($1))
LIMIT 1
`, mail).Scan(&id)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return err
}
if errors.Is(err, sql.ErrNoRows) {
newID := utils.NewUUID()
_, err = tx.Exec(`
INSERT INTO mk_mail (
id,
email,
display_name,
"type",
is_primary,
external_id,
is_active,
created_at
)
VALUES ($1, $2, '', 'user', true, true, true, NOW())
`, newID, mail)
return err
}
// Exists: normalize + activate. Avoid touching created_at.
_, err = tx.Exec(`
UPDATE mk_mail
SET
email = $2,
display_name = COALESCE(display_name, ''),
"type" = 'user',
is_primary = true,
external_id = true,
is_active = true
WHERE id::text = $1
`, id, mail)
return err
}

View File

@@ -327,6 +327,9 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
groupName string groupName string
groupTotal float64 groupTotal float64
groupTotalUSD float64 groupTotalUSD float64
nOnMLNoStr string
nOnMLDetNoStr string
hNoStr string
fiyatGirilen sql.NullFloat64 fiyatGirilen sql.NullFloat64
fiyatDoviz sql.NullString fiyatDoviz sql.NullString
maliyeteDahil sql.NullBool maliyeteDahil sql.NullBool
@@ -338,9 +341,9 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
&groupName, &groupName,
&groupTotal, &groupTotal,
&groupTotalUSD, &groupTotalUSD,
&item.NOnMLNo, &nOnMLNoStr,
&item.NOnMLDetNo, &nOnMLDetNoStr,
&item.NHammaddeTuruNo, &hNoStr,
&item.SKodu, &item.SKodu,
&item.SAciklama, &item.SAciklama,
&item.SRenk, &item.SRenk,
@@ -371,6 +374,10 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
} }
scannedRows += 1 scannedRows += 1
item.NOnMLNo, _ = strconv.Atoi(nOnMLNoStr)
item.NOnMLDetNo, _ = strconv.Atoi(nOnMLDetNoStr)
item.NHammaddeTuruNo, _ = strconv.Atoi(hNoStr)
if fiyatGirilen.Valid { if fiyatGirilen.Valid {
item.FiyatGirilen = new(float64) item.FiyatGirilen = new(float64)
*item.FiyatGirilen = fiyatGirilen.Float64 *item.FiyatGirilen = fiyatGirilen.Float64
@@ -426,8 +433,8 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
rows, err := queries.GetProductionHasCostDetailRowsByOnMLNo(ctx, uretimDB, nOnMLNo) rows, err := queries.GetProductionHasCostDetailRowsByOnMLNo(ctx, uretimDB, nOnMLNo)
if err != nil { if err != nil {
logger.Error("query error", "err", err) logger.Error("query error", "err", err)
log.Printf("❌ [ProductionHasCostDetailGroups] query error: %v", err) log.Printf(" [ProductionHasCostDetailGroups] query error: %v", err)
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError) http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
return return
} }
defer rows.Close() defer rows.Close()
@@ -442,6 +449,9 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
groupName string groupName string
groupTotal float64 groupTotal float64
groupTotalUSD float64 groupTotalUSD float64
nOnMLNoStr string
nOnMLDetNoStr string
hNoStr string
fiyatGirilen sql.NullFloat64 fiyatGirilen sql.NullFloat64
fiyatDoviz sql.NullString fiyatDoviz sql.NullString
maliyeteDahil sql.NullBool maliyeteDahil sql.NullBool
@@ -453,9 +463,9 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
&groupName, &groupName,
&groupTotal, &groupTotal,
&groupTotalUSD, &groupTotalUSD,
&item.NOnMLNo, &nOnMLNoStr,
&item.NOnMLDetNo, &nOnMLDetNoStr,
&item.NHammaddeTuruNo, &hNoStr,
&item.SKodu, &item.SKodu,
&item.SAciklama, &item.SAciklama,
&item.SRenk, &item.SRenk,
@@ -479,10 +489,16 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
&item.SHammaddeTuruAdi, &item.SHammaddeTuruAdi,
&item.SParcaAdi, &item.SParcaAdi,
); err != nil { ); err != nil {
log.Printf("⚠️ [ProductionHasCostDetailGroups] scan error: %v", err) scanErrors++
logger.Warn("scan error", "scan_index", scannedRows+scanErrors, "err", err)
log.Printf("⚠️ [ProductionHasCostDetailGroups] scan error: %v", err)
continue continue
} }
scannedRows += 1 scannedRows++
item.NOnMLNo, _ = strconv.Atoi(nOnMLNoStr)
item.NOnMLDetNo, _ = strconv.Atoi(nOnMLDetNoStr)
item.NHammaddeTuruNo, _ = strconv.Atoi(hNoStr)
if fiyatGirilen.Valid { if fiyatGirilen.Valid {
item.FiyatGirilen = new(float64) item.FiyatGirilen = new(float64)
@@ -1093,21 +1109,74 @@ func PostProductionProductCostingDefaultQuantitiesLookupHandler(w http.ResponseW
return return
} }
m, err := queries.LookupProductionProductCostingDefaultQtyByNos(ctx, uretimDB, req.NHammaddeTuruNos) rows, err := queries.LookupProductionProductCostingDefaultQtyByNos(ctx, uretimDB, req.NHammaddeTuruNos)
if err != nil { if err != nil {
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return return
} }
out := make([]models.ProductionProductCostingDefaultQtyLookupItem, 0, len(m)) out := make([]models.ProductionProductCostingDefaultQtyLookupItem, 0, len(rows))
for no, qty := range m { for _, r0 := range rows {
out = append(out, models.ProductionProductCostingDefaultQtyLookupItem{ out = append(out, models.ProductionProductCostingDefaultQtyLookupItem{
NHammaddeTuruNo: no, NHammaddeTuruNo: r0.No,
LDefaultMiktar: qty, SAciklama: r0.Aciklama,
LDefaultMiktar: r0.Qty,
}) })
} }
_ = json.NewEncoder(w).Encode(out) _ = json.NewEncoder(w).Encode(out)
} }
// POST /api/pricing/production-product-costing/has-cost-detail/last-detail
func PostProductionProductCostingHasCostDetailLastDetailHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
uretimDB := db.GetUretimDB()
if uretimDB == nil {
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
return
}
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
var req models.ProductionProductCostingLastOnMLDetLookupRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Gecersiz JSON", http.StatusBadRequest)
return
}
rows, err := queries.LookupLastOnMLMasDetByHammaddeNos(ctx, uretimDB, req.NHammaddeTuruNos, req.BeforeDate, req.ExcludeOnMLNo, req.NFirmaID, req.OnlyICode, 1)
if err != nil {
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(rows)
}
// POST /api/pricing/production-product-costing/options/hammadde-by-nos
func PostProductionProductCostingOptionsHammaddeByNosHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
uretimDB := db.GetUretimDB()
if uretimDB == nil {
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
return
}
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
var req models.ProductionProductCostingHammaddeByNosRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Gecersiz JSON", http.StatusBadRequest)
return
}
rows, err := queries.GetProductionHammaddeByNos(ctx, uretimDB, req.NHammaddeTuruNos)
if err != nil {
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(rows)
}
// POST /api/pricing/production-product-costing/default-quantities/refresh // POST /api/pricing/production-product-costing/default-quantities/refresh
func PostProductionProductCostingDefaultQuantitiesRefreshHandler(w http.ResponseWriter, r *http.Request) { func PostProductionProductCostingDefaultQuantitiesRefreshHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
@@ -1166,6 +1235,11 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
req.Header.FirmaKodu = strings.TrimSpace(req.Header.FirmaKodu) req.Header.FirmaKodu = strings.TrimSpace(req.Header.FirmaKodu)
if req.Header.UrunKodu == "" || req.Header.MaliyetTarihi == "" { if req.Header.UrunKodu == "" || req.Header.MaliyetTarihi == "" {
logger.Warn("validation failed: missing required header fields",
"trace_id", traceID,
"urun_kodu", req.Header.UrunKodu,
"maliyet_tarihi", req.Header.MaliyetTarihi,
)
http.Error(w, "urun_kodu ve maliyet_tarihi zorunlu", http.StatusBadRequest) http.Error(w, "urun_kodu ve maliyet_tarihi zorunlu", http.StatusBadRequest)
return return
} }
@@ -1210,6 +1284,11 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
firmaID = id firmaID = id
} }
if firmaID <= 0 { if firmaID <= 0 {
logger.Warn("validation failed: missing firma",
"trace_id", traceID,
"n_firma_id", req.Header.NFirmaID,
"firma_kodu", req.Header.FirmaKodu,
)
http.Error(w, "Firma secilmeden kaydedilemez (n_firma_id / firma_kodu)", http.StatusBadRequest) http.Error(w, "Firma secilmeden kaydedilemez (n_firma_id / firma_kodu)", http.StatusBadRequest)
return return
} }
@@ -1264,6 +1343,11 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
// Parse Tarihi // Parse Tarihi
tarihi, err := time.Parse("2006-01-02", req.Header.MaliyetTarihi) tarihi, err := time.Parse("2006-01-02", req.Header.MaliyetTarihi)
if err != nil { if err != nil {
logger.Warn("validation failed: maliyet_tarihi parse error",
"trace_id", traceID,
"maliyet_tarihi", req.Header.MaliyetTarihi,
"err", err,
)
http.Error(w, "maliyet_tarihi YYYY-MM-DD formatinda olmali", http.StatusBadRequest) http.Error(w, "maliyet_tarihi YYYY-MM-DD formatinda olmali", http.StatusBadRequest)
return return
} }
@@ -1274,7 +1358,23 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
http.Error(w, "Islem baslatilamadi", http.StatusInternalServerError) http.Error(w, "Islem baslatilamadi", http.StatusInternalServerError)
return return
} }
defer func() { _ = tx.Rollback() }() committed := false
logger.Info("tx begin",
"trace_id", traceID,
"n_onml_no", nOnMLNo,
"urun_kodu", req.Header.UrunKodu,
"maliyet_tarihi", req.Header.MaliyetTarihi,
)
defer func() {
if committed {
return
}
if err := tx.Rollback(); err == nil {
logger.Info("tx rollback", "trace_id", traceID, "n_onml_no", nOnMLNo)
} else {
logger.Warn("tx rollback failed", "trace_id", traceID, "n_onml_no", nOnMLNo, "err", err)
}
}()
// Determine mamul turu inside same tx (to keep create atomic) // Determine mamul turu inside same tx (to keep create atomic)
mamulLabel := "" mamulLabel := ""
@@ -1299,6 +1399,7 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
return return
} }
if mt <= 0 { if mt <= 0 {
logger.Warn("validation failed: mamul turu not found", "trace_id", traceID, "n_onml_no", nOnMLNo, "mamul_label", mamulLabel)
http.Error(w, "Mamul turu bulunamadi: "+mamulLabel, http.StatusBadRequest) http.Error(w, "Mamul turu bulunamadi: "+mamulLabel, http.StatusBadRequest)
return return
} }
@@ -1320,6 +1421,7 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
sAciklama = sql.NullString{String: req.Header.SAciklama, Valid: true} sAciklama = sql.NullString{String: req.Header.SAciklama, Valid: true}
} }
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "upsert_header")
if err := queries.UpsertOnMLHeader(tx, ctx, queries.OnMLHeaderUpsertArgs{ if err := queries.UpsertOnMLHeader(tx, ctx, queries.OnMLHeaderUpsertArgs{
NOnMLNo: nOnMLNo, NOnMLNo: nOnMLNo,
UrunKodu: req.Header.UrunKodu, UrunKodu: req.Header.UrunKodu,
@@ -1343,6 +1445,7 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
} }
// Deletes // Deletes
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "detail_deletes", "count", len(req.Detail.Deletes))
for _, d := range req.Detail.Deletes { for _, d := range req.Detail.Deletes {
if d.NOnMLDetNo <= 0 { if d.NOnMLDetNo <= 0 {
continue continue
@@ -1355,14 +1458,34 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
} }
// Upserts // Upserts
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "detail_upserts", "count", len(req.Detail.Upserts))
for _, row := range req.Detail.Upserts { for _, row := range req.Detail.Upserts {
if row.NOnMLDetNo <= 0 { if row.NOnMLDetNo <= 0 {
continue continue
} }
if row.NHammaddeTuruNo <= 0 || strings.TrimSpace(row.SKodu) == "" { if row.NHammaddeTuruNo <= 0 || strings.TrimSpace(row.SKodu) == "" {
http.Error(w, "Detay satirinda n_hammadde_turu_no ve s_kodu zorunlu", http.StatusBadRequest) // FALLBACK: If nHammaddeTuruNo is missing but sKodu is present, default to 1 (General/Labor)
// to avoid blocking the user, especially for labor items.
if strings.TrimSpace(row.SKodu) != "" && row.NHammaddeTuruNo <= 0 {
logger.Warn("n_hammadde_turu_no <= 0, falling back to 1",
"trace_id", traceID,
"n_onml_no", nOnMLNo,
"n_onml_det_no", row.NOnMLDetNo,
"s_kodu", strings.TrimSpace(row.SKodu),
)
row.NHammaddeTuruNo = 1
} else {
logger.Warn("validation failed: missing required detail fields (s_kodu empty)",
"trace_id", traceID,
"n_onml_no", nOnMLNo,
"n_onml_det_no", row.NOnMLDetNo,
"n_hammadde_turu_no", row.NHammaddeTuruNo,
"s_kodu", strings.TrimSpace(row.SKodu),
)
http.Error(w, "Detay satirinda s_kodu zorunlu", http.StatusBadRequest)
return return
} }
}
qty := row.LMiktar qty := row.LMiktar
if qty < 0 { if qty < 0 {
qty = 0 qty = 0
@@ -1387,9 +1510,21 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
lTutar := unitTRY * qty lTutar := unitTRY * qty
lDovizTutari := unitUSD * qty lDovizTutari := unitUSD * qty
// Debug log for price resolution
logger.Info("price debug",
"s_kodu", strings.TrimSpace(row.SKodu),
"qty", qty,
"fiyat_girilen", row.FiyatGirilen,
"fiyat_doviz", strings.TrimSpace(row.FiyatDoviz),
"unitTRY", unitTRY,
"lTutar", lTutar,
"lDovizTutari", lDovizTutari,
)
// Resolve stock type id from tbStok by sKodu (exact), then fallback to model-based match. // Resolve stock type id from tbStok by sKodu (exact), then fallback to model-based match.
// Note: In this DB, stock type is stored as tbStok.nStokTipi but spUrtOnMLMasDet expects nStokTipiID (int). // Note: In this DB, stock type is stored as tbStok.nStokTipi but spUrtOnMLMasDet expects nStokTipiID (int).
rawSKodu := strings.TrimSpace(row.SKodu) rawSKodu := strings.TrimSpace(row.SKodu)
logger.Info("resolving stock type", "s_kodu", rawSKodu)
var nStokTipiID int var nStokTipiID int
err := tx.QueryRowContext(ctx, ` err := tx.QueryRowContext(ctx, `
SELECT TOP 1 ISNULL(CONVERT(int, ISNULL(S.nStokTipi, 0)), 0) AS nStokTipiID SELECT TOP 1 ISNULL(CONVERT(int, ISNULL(S.nStokTipi, 0)), 0) AS nStokTipiID
@@ -1411,16 +1546,32 @@ ORDER BY
`, rawSKodu).Scan(&nStokTipiID) `, rawSKodu).Scan(&nStokTipiID)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
http.Error(w, "Stok tipi bulunamadi (s_kodu="+rawSKodu+")", http.StatusBadRequest) // FALLBACK: If stock item not found in tbStok at all, default to 1.
return logger.Warn("stok tipi not found in tbStok, falling back to 1",
} "trace_id", traceID,
"n_onml_no", nOnMLNo,
"n_onml_det_no", row.NOnMLDetNo,
"s_kodu", rawSKodu,
)
nStokTipiID = 1
} else {
logger.Error("stok tipi lookup error", "err", err) logger.Error("stok tipi lookup error", "err", err)
http.Error(w, "Stok tipi bulunamadi (tbStok sorgu hatasi)", http.StatusInternalServerError) http.Error(w, "Stok tipi bulunamadi (tbStok sorgu hatasi)", http.StatusInternalServerError)
return return
} }
}
logger.Info("stock type resolved", "s_kodu", rawSKodu, "n_stok_tipi_id", nStokTipiID)
if nStokTipiID <= 0 { if nStokTipiID <= 0 {
http.Error(w, "Stok tipi bulunamadi (s_kodu="+rawSKodu+")", http.StatusBadRequest) // FALLBACK: If stock type is missing or 0 in tbStok, default to 1 (usually 'Raw Material' or 'General').
return // This prevents blocking the save process for items not fully configured in tbStok.
logger.Warn("stok tipi <= 0, falling back to 1",
"trace_id", traceID,
"n_onml_no", nOnMLNo,
"n_onml_det_no", row.NOnMLDetNo,
"s_kodu", rawSKodu,
"n_stok_tipi_id_orig", nStokTipiID,
)
nStokTipiID = 1
} }
// Dummy/system-mapped required fields: // Dummy/system-mapped required fields:
@@ -1469,7 +1620,8 @@ WHEN MATCHED THEN
Maliyete_dahil = @p29, Maliyete_dahil = @p29,
cm_price_type_id = @p30, cm_price_type_id = @p30,
sKullaniciAdiDeg = @p31, sKullaniciAdiDeg = @p31,
dteIslemTarihiDeg = GETDATE() dteIslemTarihiDeg = GETDATE(),
sAciklama3 = NULLIF(@p32, '')
WHEN NOT MATCHED THEN WHEN NOT MATCHED THEN
INSERT ( INSERT (
nOnMLNo,nOnMLDetNo,nHammaddeTuruNo,sKodu,sAciklama,sRenk,lMiktar,lFiyat,lTutar, nOnMLNo,nOnMLDetNo,nHammaddeTuruNo,sKodu,sAciklama,sRenk,lMiktar,lFiyat,lTutar,
@@ -1480,7 +1632,7 @@ WHEN NOT MATCHED THEN
VALUES ( VALUES (
@p1,@p2,@p3,@p4,@p5,@p6,@p9,@p10,@p11, @p1,@p2,@p3,@p4,@p5,@p6,@p9,@p10,@p11,
@p12,@p13,@p14,@p15,@p16,@p17,@p18,@p19, @p12,@p13,@p14,@p15,@p16,@p17,@p18,@p19,
@p20,NULLIF(@p8,''),@p21,@p22,@p31,GETDATE(),NULLIF(@p7,''),NULL,@p23,@p24, @p20,NULLIF(@p8,''),@p21,@p22,@p31,GETDATE(),NULLIF(@p7,''),NULLIF(@p32, ''),@p23,@p24,
@p25,@p26,NULLIF(@p27,0),NULLIF(@p28,''),@p29,@p30 @p25,@p26,NULLIF(@p27,0),NULLIF(@p28,''),@p29,@p30
); );
` `
@@ -1519,8 +1671,21 @@ WHEN NOT MATCHED THEN
row.MaliyeteDahil, row.MaliyeteDahil,
row.CMPriceTypeID, row.CMPriceTypeID,
user, user,
strings.TrimSpace(row.SAciklama3), // p32: sAciklama3 (Grup Adi)
); err != nil { ); err != nil {
logger.Error("detail merge error", "err", err) logger.Error("detail merge error",
"trace_id", traceID,
"n_onml_no", nOnMLNo,
"n_onml_det_no", row.NOnMLDetNo,
"n_hammadde_turu_no", row.NHammaddeTuruNo,
"n_urt_mt_bolum_id", row.NUrtMTBolumID,
"s_kodu", strings.TrimSpace(row.SKodu),
"fiyat_girilen", row.FiyatGirilen,
"fiyat_doviz", strings.TrimSpace(row.FiyatDoviz),
"l_miktar", qty,
"n_stok_tipi_id", nStokTipiID,
"err", err,
)
http.Error(w, "Detay kaydedilemedi", http.StatusInternalServerError) http.Error(w, "Detay kaydedilemedi", http.StatusInternalServerError)
return return
} }
@@ -1533,6 +1698,7 @@ WHEN NOT MATCHED THEN
// ============================================================ // ============================================================
if req.Header.NUrtReceteID > 0 { if req.Header.NUrtReceteID > 0 {
receteID := req.Header.NUrtReceteID receteID := req.Header.NUrtReceteID
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "recipe_sync", "n_urt_recete_id", receteID)
// Determine next available recipe detail id (nUrtRecMBolumID) // Determine next available recipe detail id (nUrtRecMBolumID)
nextRecDetID := 0 nextRecDetID := 0
@@ -1551,94 +1717,72 @@ WHERE RMik.nUrtReceteID = @p1
continue continue
} }
// CM1/CM2 rows are cost-only and must NOT be written back into recipe tables. // 1. FILTER: CM1/CM2 (Labor/Service) rows must NOT be written back into recipe tables.
// Source of truth: spUrtOnMLHammaddeTuru.sAciklama3 (e.g. 'CM2'). // We check the group label (sAciklama3) from the row itself.
var grp sql.NullString g := strings.ToUpper(strings.TrimSpace(row.SAciklama3))
if err := tx.QueryRowContext(ctx, `
SELECT TOP 1 LTRIM(RTRIM(ISNULL(H.sAciklama3, '')))
FROM dbo.spUrtOnMLHammaddeTuru H WITH (NOLOCK)
WHERE H.nHammaddeTuruNo = @p1
`, hNo).Scan(&grp); err == nil {
g := strings.ToUpper(strings.TrimSpace(grp.String))
if g == "CM1" || g == "CM2" { if g == "CM1" || g == "CM2" {
logger.Info("recipe sync skip: labor item", "s_kodu", row.SKodu, "group", g)
continue continue
} }
}
// Resolve nHStokID from tbStok using sKodu (exact), then sModel fallback. // In this version of URETIM DB, the table name is dbo.spUrtRecMBolumMik
// IMPORTANT: nHStokID must be resolved; otherwise we'd update/insert too broadly. // but it uses _G suffixes for quantity and amount columns.
// Also nHStokID_G stores the stock code string rather than just an ID.
rawSKodu := strings.TrimSpace(row.SKodu) rawSKodu := strings.TrimSpace(row.SKodu)
var nHStokID int
err := tx.QueryRowContext(ctx, ` // Ensure a section entry (spUrtRecMBolum) exists for this hNo (Hammadde Turu)
SELECT TOP 1 ISNULL(S.nStokID, 0) // in the current recipe, otherwise detail rows (Mik) cannot be linked properly.
FROM dbo.tbStok S WITH (NOLOCK) var sectionExists int
WHERE ISNULL(S.IsBlocked, 0) = 0 _ = tx.QueryRowContext(ctx, `
AND ( SELECT COUNT(1) FROM dbo.spUrtRecMBolum WITH (NOLOCK)
REPLACE(LTRIM(RTRIM(ISNULL(S.sKodu,''))), ' ', '') = REPLACE(@p1, ' ', '') WHERE nUrtReceteID = @p1 AND nUrtMBolumID = @p2
OR LTRIM(RTRIM(ISNULL(S.sModel,''))) = @p1 `, receteID, hNo).Scan(&sectionExists)
OR @p1 LIKE LTRIM(RTRIM(ISNULL(S.sModel,''))) + '%'
) if sectionExists <= 0 {
ORDER BY logger.Info("creating missing recipe section", "n_urt_recete_id", receteID, "n_urt_m_bolum_id", hNo)
CASE _, _ = tx.ExecContext(ctx, `
WHEN REPLACE(LTRIM(RTRIM(ISNULL(S.sKodu,''))), ' ', '') = REPLACE(@p1, ' ', '') THEN 0 INSERT INTO dbo.spUrtRecMBolum (nUrtReceteID, nUrtUBolumID, nUrtMBolumID, nUrtMTBolumID, sKullaniciAdi, dteIslemTarihi)
WHEN LTRIM(RTRIM(ISNULL(S.sModel,''))) = @p1 THEN 1 VALUES (@p1, 13, @p2, @p3, @p4, GETDATE())
ELSE 2 `, receteID, hNo, row.NUrtMTBolumID, user)
END,
S.dteKayitTarihi DESC,
S.nStokID DESC
`, rawSKodu).Scan(&nHStokID)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Recete sync icin stok bulunamadi (s_kodu="+rawSKodu+")", http.StatusBadRequest)
return
}
logger.Error("recipe stok lookup error", "err", err)
http.Error(w, "Recete sync stok bulunamadi", http.StatusInternalServerError)
return
}
if nHStokID == 0 {
http.Error(w, "Recete sync icin stok bulunamadi (s_kodu="+rawSKodu+")", http.StatusBadRequest)
return
} }
// If we cannot resolve stock, we still try to insert by hammadde no only. // Update quantity and prices if row already exists for (recete, hammadde, stok_code)
// Some recipes may omit nHStokID; but we prefer filling it when possible. // Using nHStokID_G (string code) for matching as per user screenshot.
// Update quantity if row already exists for (recete, hammadde, stok)
var exists int var exists int
if err := tx.QueryRowContext(ctx, ` if err := tx.QueryRowContext(ctx, `
SELECT COUNT(1) SELECT COUNT(1)
FROM dbo.spUrtRecMBolumMik RMik WITH (NOLOCK) FROM dbo.spUrtRecMBolumMik RMik WITH (NOLOCK)
WHERE RMik.nUrtReceteID = @p1 WHERE RMik.nUrtReceteID = @p1
AND RMik.nUrtMBolumID = @p2 AND RMik.nUrtMBolumID = @p2
AND RMik.nHStokID = @p3 AND LTRIM(RTRIM(RMik.nHStokID_G)) = @p3
`, receteID, hNo, nHStokID).Scan(&exists); err == nil && exists > 0 { `, receteID, hNo, rawSKodu).Scan(&exists); err == nil && exists > 0 {
_, _ = tx.ExecContext(ctx, ` _, _ = tx.ExecContext(ctx, `
UPDATE dbo.spUrtRecMBolumMik UPDATE dbo.spUrtRecMBolumMik
SET lHMiktar = @p4, SET lHMiktar_G = @p4,
sKullaniciAdiDeg = @p5, lHMaliyet_G = @p5,
dteIslemTarihiDeg = GETDATE() sKullaniciAdiDeg = @p6,
dteIslemTarihiDeg = GETDATE(),
bIslem = @p7
WHERE nUrtReceteID = @p1 WHERE nUrtReceteID = @p1
AND nUrtMBolumID = @p2 AND nUrtMBolumID = @p2
AND nHStokID = @p3 AND LTRIM(RTRIM(nHStokID_G)) = @p3
`, receteID, hNo, nHStokID, row.LMiktar, user) `, receteID, hNo, rawSKodu, row.LMiktar, unitTRY, user, row.MaliyeteDahil)
continue continue
} }
// Insert missing: best-effort with minimal columns. // Insert missing: using _G columns and storing code in nHStokID_G.
// NOTE: This assumes nUrtRecMBolumID can be a sequential int and that other columns are nullable/have defaults.
_, insertErr := tx.ExecContext(ctx, ` _, insertErr := tx.ExecContext(ctx, `
INSERT INTO dbo.spUrtRecMBolumMik ( INSERT INTO dbo.spUrtRecMBolumMik (
nUrtReceteID, nUrtReceteID,
nUrtUBolumID, nUrtUBolumID,
nUrtRecMBolumID, nUrtRecMBolumID,
nStokID, nStokID,
nHStokID, nHStokID_G,
lHMiktar, lHMiktar_G,
lHFire, lHFire_G,
nMaliyetTipiID, nMaliyetTipiID,
lHMaliyet, lHMaliyet_G,
lMiktar, lMiktar_G,
sIslemKodu, sIslemKodu,
nUrtMBolumID, nUrtMBolumID,
nUrtMTBolumID, nUrtMTBolumID,
@@ -1646,56 +1790,52 @@ INSERT INTO dbo.spUrtRecMBolumMik (
bIslem, bIslem,
nSure, nSure,
sAciklama, sAciklama,
dteDovizTarihi,
sDovizCinsi,
lDovizOran,
lDovizFiyat,
sKullaniciAdi, sKullaniciAdi,
dteIslemTarihi, dteIslemTarihi,
nMBolumSarfTipiNo nMBolumSarfTipiNo
) )
VALUES ( VALUES (
@p1, @p1,
0, 13,
@p2, @p2,
0, 0,
@p3, @p3, -- nHStokID_G (Code)
@p4, @p4, -- lHMiktar_G
0, 0,
6, 6,
0, @p5, -- lHMaliyet_G
1, 1, -- lMiktar_G
'', '',
@p5,
@p6, @p6,
1,
0,
0,
NULL,
NULL,
NULL,
NULL,
NULL,
@p7, @p7,
1,
@p8, -- bIslem
0,
NULL,
@p9,
GETDATE(), GETDATE(),
1 1
) )
`, receteID, nextRecDetID, nHStokID, row.LMiktar, hNo, row.NUrtMTBolumID, user) `, receteID, nextRecDetID, rawSKodu, row.LMiktar, unitTRY, hNo, row.NUrtMTBolumID, row.MaliyeteDahil, user)
if insertErr == nil { if insertErr == nil {
nextRecDetID += 1 nextRecDetID += 1
} }
} }
} }
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "commit")
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
logger.Error("tx commit error", "err", err) logger.Error("tx commit error", "err", err)
http.Error(w, "Kaydetme tamamlanamadi", http.StatusInternalServerError) http.Error(w, "Kaydetme tamamlanamadi", http.StatusInternalServerError)
return return
} }
committed = true
logger.Info("tx commit ok", "trace_id", traceID, "n_onml_no", nOnMLNo)
// V3: update base price table so pricing screens reflect latest costing. // V3: update base price table so pricing screens reflect latest costing.
// Not transactional with URETIM DB; if this fails, URETIM save has already succeeded. // Not transactional with URETIM DB; if this fails, URETIM save has already succeeded.
if mssqlDB != nil { if mssqlDB != nil {
logger.Info("post-commit step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "v3_base_price_upsert")
if err := queries.UpsertV3ItemBasePriceUSD(ctx, mssqlDB, req.Header.UrunKodu, req.Header.MaliyetTarihi, totalUSD, user); err != nil { if err := queries.UpsertV3ItemBasePriceUSD(ctx, mssqlDB, req.Header.UrunKodu, req.Header.MaliyetTarihi, totalUSD, user); err != nil {
logger.Error("v3 base price upsert error", "err", err) logger.Error("v3 base price upsert error", "err", err)
http.Error(w, "URETIM kaydedildi ama V3 maliyet guncellenemedi", http.StatusInternalServerError) http.Error(w, "URETIM kaydedildi ama V3 maliyet guncellenemedi", http.StatusInternalServerError)
@@ -1706,6 +1846,131 @@ VALUES (
_ = json.NewEncoder(w).Encode(models.ProductionProductCostingOnMLSaveResponse{NOnMLNo: nOnMLNo}) _ = json.NewEncoder(w).Encode(models.ProductionProductCostingOnMLSaveResponse{NOnMLNo: nOnMLNo})
} }
// POST /api/pricing/production-product-costing/onml/delete
// Deletes costing records created in URETIM (OnML header + details) and, if created by this app, V3 base price row.
// IMPORTANT: Recipe tables are NOT touched.
func PostProductionProductCostingOnMLDeleteHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
uretimDB := db.GetUretimDB()
if uretimDB == nil {
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
return
}
mssqlDB := db.GetDB()
claims, _ := auth.GetClaimsFromContext(r.Context())
user := ""
if claims != nil {
user = strings.TrimSpace(claims.Username)
}
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.onml.delete")
var req models.ProductionProductCostingOnMLDeleteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Gecersiz JSON", http.StatusBadRequest)
return
}
if req.NOnMLNo <= 0 {
http.Error(w, "n_onml_no zorunlu", http.StatusBadRequest)
return
}
// Load header info for safe deletes and redirect behavior.
var urunKodu string
var maliyetTarihi time.Time
err := uretimDB.QueryRowContext(ctx, `
SELECT TOP 1
LTRIM(RTRIM(ISNULL(UrunKodu,''))) AS UrunKodu,
COALESCE(Tarihi, dteKayitTarihi, GETDATE()) AS Tarihi
FROM dbo.spUrtOnMLMas WITH (NOLOCK)
WHERE nOnMLNo = @p1
`, req.NOnMLNo).Scan(&urunKodu, &maliyetTarihi)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Kayit bulunamadi", http.StatusNotFound)
return
}
logger.Error("header lookup error", "err", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
urunKodu = strings.TrimSpace(urunKodu)
tx, err := uretimDB.BeginTx(ctx, nil)
if err != nil {
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
defer func() { _ = tx.Rollback() }()
// Delete details first, then header.
if _, err := tx.ExecContext(ctx, `
DELETE FROM dbo.spUrtOnMLMasDet WHERE nOnMLNo = @p1
`, req.NOnMLNo); err != nil {
logger.Error("delete detail error", "err", err)
http.Error(w, "Detay silinemedi", http.StatusInternalServerError)
return
}
if _, err := tx.ExecContext(ctx, `
DELETE FROM dbo.spUrtOnMLMas WHERE nOnMLNo = @p1
`, req.NOnMLNo); err != nil {
logger.Error("delete header error", "err", err)
http.Error(w, "Header silinemedi", http.StatusInternalServerError)
return
}
if err := tx.Commit(); err != nil {
logger.Error("tx commit error", "err", err)
http.Error(w, "Silme tamamlanamadi", http.StatusInternalServerError)
return
}
// V3: Delete base price ONLY if row was created by this app (CreatedUserName starts with BSSAPP).
deletedBasePrice := false
if mssqlDB != nil && urunKodu != "" {
var createdBy sql.NullString
var lastBy sql.NullString
_ = mssqlDB.QueryRowContext(ctx, `
SELECT TOP 1
ISNULL(CreatedUserName,'') AS CreatedUserName,
ISNULL(LastUpdatedUserName,'') AS LastUpdatedUserName
FROM dbo.prItemBasePrice WITH (NOLOCK)
WHERE ItemTypeCode = 1
AND LTRIM(RTRIM(ItemCode)) = @p1
AND ISNULL(CountryCode,'') = 'TR'
AND ISNULL(SeasonCode,'') = ''
AND ISNULL(BasePriceCode,0) = 1
`, urunKodu).Scan(&createdBy, &lastBy)
created := strings.ToUpper(strings.TrimSpace(createdBy.String))
last := strings.ToUpper(strings.TrimSpace(lastBy.String))
if strings.HasPrefix(created, "BSSAPP") && strings.HasPrefix(last, "BSSAPP") {
if _, err := mssqlDB.ExecContext(ctx, `
DELETE FROM dbo.prItemBasePrice
WHERE ItemTypeCode = 1
AND LTRIM(RTRIM(ItemCode)) = @p1
AND ISNULL(CountryCode,'') = 'TR'
AND ISNULL(SeasonCode,'') = ''
AND ISNULL(BasePriceCode,0) = 1
`, urunKodu); err == nil {
deletedBasePrice = true
}
}
}
logger.Info("delete done", "n_onml_no", req.NOnMLNo, "urun_kodu", urunKodu, "deleted_base_price", deletedBasePrice, "user", user)
_ = json.NewEncoder(w).Encode(map[string]any{
"ok": true,
"n_onml_no": req.NOnMLNo,
"urun_kodu": urunKodu,
"deleted_baseprice": deletedBasePrice,
})
}
// GET /api/pricing/production-product-costing/has-cost-detail-exchange-rates // GET /api/pricing/production-product-costing/has-cost-detail-exchange-rates
func GetProductionHasCostDetailExchangeRatesHandler(w http.ResponseWriter, r *http.Request) { func GetProductionHasCostDetailExchangeRatesHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")

View File

@@ -251,6 +251,13 @@ func handleUserUpdate(db *sql.DB, w http.ResponseWriter, r *http.Request, userID
return return
} }
// Keep mk_mail in sync for downstream mail mapping screens.
if err := ensureMkMail(tx, payload.Email); err != nil {
log.Printf("ERROR [UserDetail] ensureMkMail failed user_id=%d email=%q err=%v", userID, payload.Email, err)
http.Error(w, "Mail kaydi guncellenemedi", http.StatusInternalServerError)
return
}
if _, err := tx.Exec(`DELETE FROM dfrole_usr WHERE dfusr_id = $1`, userID); err != nil { if _, err := tx.Exec(`DELETE FROM dfrole_usr WHERE dfusr_id = $1`, userID); err != nil {
log.Printf("❌ [UserDetail] delete roles failed user_id=%d err=%v", userID, err) log.Printf("❌ [UserDetail] delete roles failed user_id=%d err=%v", userID, err)
http.Error(w, "Roller temizlenemedi", http.StatusInternalServerError) http.Error(w, "Roller temizlenemedi", http.StatusInternalServerError)

View File

@@ -1,75 +0,0 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import { Quasar } from 'quasar'
import { markRaw } from 'vue'
import RootComponent from 'app/src/App.vue'
import createStore from 'app/src/stores/index'
import createRouter from 'app/src/router/index'
export default async function (createAppFn, quasarUserOptions) {
// Create the app instance.
// Here we inject into it the Quasar UI, the router & possibly the store.
const app = createAppFn(RootComponent)
app.use(Quasar, quasarUserOptions)
const store = typeof createStore === 'function'
? await createStore({})
: createStore
app.use(store)
const router = markRaw(
typeof createRouter === 'function'
? await createRouter({store})
: createRouter
)
// make router instance available in store
store.use(({ store }) => { store.router = router })
// Expose the app, the router and the store.
// Note that we are not mounting the app here, since bootstrapping will be
// different depending on whether we are in a browser or on the server.
return {
app,
store,
router
}
}

View File

@@ -1,158 +0,0 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import { createApp } from 'vue'
import '@quasar/extras/roboto-font/roboto-font.css'
import '@quasar/extras/material-icons/material-icons.css'
// We load Quasar stylesheet file
import 'quasar/dist/quasar.sass'
import 'src/css/app.css'
import createQuasarApp from './app.js'
import quasarUserOptions from './quasar-user-options.js'
const publicPath = `/`
async function start ({
app,
router
, store
}, bootFiles) {
let hasRedirected = false
const getRedirectUrl = url => {
try { return router.resolve(url).href }
catch (err) {}
return Object(url) === url
? null
: url
}
const redirect = url => {
hasRedirected = true
if (typeof url === 'string' && /^https?:\/\//.test(url)) {
window.location.href = url
return
}
const href = getRedirectUrl(url)
// continue if we didn't fail to resolve the url
if (href !== null) {
window.location.href = href
window.location.reload()
}
}
const urlPath = window.location.href.replace(window.location.origin, '')
for (let i = 0; hasRedirected === false && i < bootFiles.length; i++) {
try {
await bootFiles[i]({
app,
router,
store,
ssrContext: null,
redirect,
urlPath,
publicPath
})
}
catch (err) {
if (err && err.url) {
redirect(err.url)
return
}
console.error('[Quasar] boot error:', err)
return
}
}
if (hasRedirected === true) return
app.use(router)
app.mount('#q-app')
}
createQuasarApp(createApp, quasarUserOptions)
.then(app => {
// eventually remove this when Cordova/Capacitor/Electron support becomes old
const [ method, mapFn ] = Promise.allSettled !== void 0
? [
'allSettled',
bootFiles => bootFiles.map(result => {
if (result.status === 'rejected') {
console.error('[Quasar] boot error:', result.reason)
return
}
return result.value.default
})
]
: [
'all',
bootFiles => bootFiles.map(entry => entry.default)
]
return Promise[ method ]([
import(/* webpackMode: "eager" */ 'boot/dayjs'),
import(/* webpackMode: "eager" */ 'boot/locale'),
import(/* webpackMode: "eager" */ 'boot/resizeObserverGuard')
]).then(bootFiles => {
const boot = mapFn(bootFiles).filter(entry => typeof entry === 'function')
start(app, boot)
})
})

View File

@@ -1,116 +0,0 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import App from 'app/src/App.vue'
let appPrefetch = typeof App.preFetch === 'function'
? App.preFetch
: (
// Class components return the component options (and the preFetch hook) inside __c property
App.__c !== void 0 && typeof App.__c.preFetch === 'function'
? App.__c.preFetch
: false
)
function getMatchedComponents (to, router) {
const route = to
? (to.matched ? to : router.resolve(to).route)
: router.currentRoute.value
if (!route) { return [] }
const matched = route.matched.filter(m => m.components !== void 0)
if (matched.length === 0) { return [] }
return Array.prototype.concat.apply([], matched.map(m => {
return Object.keys(m.components).map(key => {
const comp = m.components[key]
return {
path: m.path,
c: comp
}
})
}))
}
export function addPreFetchHooks ({ router, store, publicPath }) {
// Add router hook for handling preFetch.
// Doing it after initial route is resolved so that we don't double-fetch
// the data that we already have. Using router.beforeResolve() so that all
// async components are resolved.
router.beforeResolve((to, from, next) => {
const
urlPath = window.location.href.replace(window.location.origin, ''),
matched = getMatchedComponents(to, router),
prevMatched = getMatchedComponents(from, router)
let diffed = false
const preFetchList = matched
.filter((m, i) => {
return diffed || (diffed = (
!prevMatched[i] ||
prevMatched[i].c !== m.c ||
m.path.indexOf('/:') > -1 // does it has params?
))
})
.filter(m => m.c !== void 0 && (
typeof m.c.preFetch === 'function'
// Class components return the component options (and the preFetch hook) inside __c property
|| (m.c.__c !== void 0 && typeof m.c.__c.preFetch === 'function')
))
.map(m => m.c.__c !== void 0 ? m.c.__c.preFetch : m.c.preFetch)
if (appPrefetch !== false) {
preFetchList.unshift(appPrefetch)
appPrefetch = false
}
if (preFetchList.length === 0) {
return next()
}
let hasRedirected = false
const redirect = url => {
hasRedirected = true
next(url)
}
const proceed = () => {
if (hasRedirected === false) { next() }
}
preFetchList.reduce(
(promise, preFetch) => promise.then(() => hasRedirected === false && preFetch({
store,
currentRoute: to,
previousRoute: from,
redirect,
urlPath,
publicPath
})),
Promise.resolve()
)
.then(proceed)
.catch(e => {
console.error(e)
proceed()
})
})
}

View File

@@ -1,23 +0,0 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import lang from 'quasar/lang/tr.js'
import {Loading,Dialog,Notify} from 'quasar'
export default { config: {"notify":{"position":"top","timeout":2500}},lang,plugins: {Loading,Dialog,Notify} }

View File

@@ -1,6 +1,6 @@
<template> <template>
<q-page class="q-pa-md"> <q-page class="pcdq-page q-pa-md">
<div class="sticky-stack"> <div class="pcdq-top sticky-top">
<div class="save-toolbar"> <div class="save-toolbar">
<div class="row items-center justify-between q-col-gutter-sm"> <div class="row items-center justify-between q-col-gutter-sm">
<div class="col-auto text-weight-bold">Maliyet Varsayilan Miktarlar</div> <div class="col-auto text-weight-bold">Maliyet Varsayilan Miktarlar</div>
@@ -52,7 +52,9 @@
{{ error }} {{ error }}
</q-banner> </q-banner>
<div class="pcdq-table-wrap">
<q-table <q-table
class="pcdq-table"
flat flat
bordered bordered
dense dense
@@ -62,6 +64,7 @@
:loading="loading" :loading="loading"
:rows-per-page-options="[0]" :rows-per-page-options="[0]"
hide-bottom hide-bottom
sticky-header
> >
<template #body-cell-actions="props"> <template #body-cell-actions="props">
<q-td :props="props"> <q-td :props="props">
@@ -89,15 +92,8 @@
/> />
</q-td> </q-td>
</template> </template>
<template #body-cell-bAktif="props">
<q-td :props="props">
<q-toggle
:model-value="Boolean(props.row.bAktif)"
@update:model-value="val => onEditActive(props.row, val)"
/>
</q-td>
</template>
</q-table> </q-table>
</div>
</q-page> </q-page>
</template> </template>
@@ -120,7 +116,6 @@ const columns = [
{ name: 'sAciklama', label: 'Aciklama', field: 'sAciklama', align: 'left', sortable: true }, { name: 'sAciklama', label: 'Aciklama', field: 'sAciklama', align: 'left', sortable: true },
{ name: 'lDefaultMiktar', label: 'Varsayilan Miktar', field: 'lDefaultMiktar', align: 'right', sortable: true }, { name: 'lDefaultMiktar', label: 'Varsayilan Miktar', field: 'lDefaultMiktar', align: 'right', sortable: true },
{ name: 'dteCalcTarihi', label: 'Hesap Tarihi', field: 'dteCalcTarihi', align: 'left', sortable: true }, { name: 'dteCalcTarihi', label: 'Hesap Tarihi', field: 'dteCalcTarihi', align: 'left', sortable: true },
{ name: 'bAktif', label: 'Aktif', field: 'bAktif', align: 'center', sortable: true },
{ name: 'actions', label: '', field: '__actions', align: 'right', sortable: false } { name: 'actions', label: '', field: '__actions', align: 'right', sortable: false }
] ]
@@ -145,13 +140,6 @@ function onEditQty (row, val) {
row.lDefaultMiktar = qty row.lDefaultMiktar = qty
} }
function onEditActive (row, val) {
const no = Number(row?.nHammaddeTuruNo || 0)
if (!(no > 0)) return
store.setDraft(no, { bAktif: Boolean(val) })
row.bAktif = Boolean(val)
}
async function onCalcAvg (row) { async function onCalcAvg (row) {
const no = Number(row?.nHammaddeTuruNo || 0) const no = Number(row?.nHammaddeTuruNo || 0)
if (!(no > 0)) return if (!(no > 0)) return
@@ -204,7 +192,7 @@ function ensureBeforeUnloadGuard (enabled) {
} }
} }
watch(hasUnsavedChanges, (v) => ensureBeforeUnloadGuard(Boolean(v))) watch(() => Boolean(hasUnsavedChanges.value), (v) => ensureBeforeUnloadGuard(Boolean(v)))
onBeforeRouteLeave((to, from, next) => { onBeforeRouteLeave((to, from, next) => {
if (!hasUnsavedChanges.value) return next() if (!hasUnsavedChanges.value) return next()
@@ -225,11 +213,62 @@ onUnmounted(() => {
</script> </script>
<style scoped> <style scoped>
.sticky-stack { .pcdq-page {
background: #fafafa;
display: flex;
flex-direction: column;
height: calc(100vh - 56px);
min-height: 0;
overflow: hidden;
padding-top: 10px;
}
.sticky-top {
flex: 0 0 auto;
z-index: 10;
background: #fafafa;
}
.pcdq-top {
flex: 0 0 auto;
}
.pcdq-table-wrap {
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.pcdq-table {
width: 100%;
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
}
.pcdq-table :deep(.q-table__container) {
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-height: 0;
}
.pcdq-table :deep(.q-table__middle) {
flex: 1 1 auto;
min-height: 0;
overflow: auto !important;
}
.pcdq-table :deep(.q-table thead th) {
background: #f0f0f0;
}
.pcdq-table :deep(.q-table__middle thead tr th) {
position: sticky; position: sticky;
top: var(--header-h); top: 0;
z-index: 100; z-index: 10;
background: #fff;
padding-top: 8px;
} }
</style> </style>

View File

@@ -51,7 +51,7 @@
color="primary" color="primary"
class="pcd-toolbar-btn" class="pcd-toolbar-btn"
:loading="detailLoading" :loading="detailLoading"
@click="fetchDetail({ clearDraft: true, hydrateDraft: false })" @click="confirmRefresh"
/> />
<q-btn <q-btn
label="Toplu Fiyat Cagir" label="Toplu Fiyat Cagir"
@@ -84,6 +84,19 @@
:disable="!detailHeader || detailLoading || saveLoading || bulkPriceLoading" :disable="!detailHeader || detailLoading || saveLoading || bulkPriceLoading"
@click="saveChanges" @click="saveChanges"
/> />
<q-btn
v-if="isNoCostDetail"
label="Kaydi Sil"
icon="delete"
dense
color="negative"
outline
class="pcd-toolbar-btn"
:disable="!canDeleteCosting || detailLoading || saveLoading || bulkPriceLoading"
@click="deleteCosting"
>
<q-tooltip v-if="!canDeleteCosting">Once maliyet olusturulunca aktif olur</q-tooltip>
</q-btn>
</div> </div>
</div> </div>
</div> </div>
@@ -359,13 +372,19 @@
</template> </template>
<template #body-cell-sKodu="props"> <template #body-cell-sKodu="props">
<q-td :props="props"> <q-td
:props="props"
:class="resolveAutoOrICodeHighlightClass(props.row)"
>
<span>{{ props.value }}</span> <span>{{ props.value }}</span>
</q-td> </q-td>
</template> </template>
<template #body-cell-sAciklama="props"> <template #body-cell-sAciklama="props">
<q-td :props="props"> <q-td
:props="props"
:class="resolveAutoOrICodeHighlightClass(props.row)"
>
<span>{{ props.value }}</span> <span>{{ props.value }}</span>
</q-td> </q-td>
</template> </template>
@@ -443,9 +462,12 @@
</div> </div>
</q-page> </q-page>
<q-dialog v-model="rowEditorDialogOpen" persistent> <q-dialog v-model="rowEditorDialogOpen" persistent @show="resetRowEditorDialogPosition" @hide="stopRowEditorDialogDrag">
<q-card class="pcd-row-editor-dialog"> <q-card class="pcd-row-editor-dialog" :style="rowEditorDialogStyle">
<q-card-section class="row items-center justify-between q-pb-sm"> <q-card-section
class="row items-center justify-between q-pb-sm pcd-row-editor-drag-handle"
@mousedown.prevent.stop="startRowEditorDialogDrag"
>
<div class="text-subtitle1 text-weight-bold"> <div class="text-subtitle1 text-weight-bold">
{{ rowEditorMode === 'edit' ? 'Satir Duzenle' : 'Yeni Satir Ekle' }} {{ rowEditorMode === 'edit' ? 'Satir Duzenle' : 'Yeni Satir Ekle' }}
</div> </div>
@@ -752,6 +774,47 @@ const rowEditorDialogOpen = ref(false)
const rowEditorMode = ref('new') const rowEditorMode = ref('new')
const rowEditorTargetRowKey = ref('') const rowEditorTargetRowKey = ref('')
const rowEditorForm = ref(createRowEditorForm()) const rowEditorForm = ref(createRowEditorForm())
// Draggable "Satir Duzenle" dialog (mouse drag by header).
const rowEditorDialogPos = ref({ x: 0, y: 0 })
const rowEditorDialogPanBase = ref({ x: 0, y: 0 })
const rowEditorDialogDragging = ref(false)
const rowEditorDialogDragMouseStart = ref({ x: 0, y: 0 })
const rowEditorDialogDragPosStart = ref({ x: 0, y: 0 })
const rowEditorDialogStyle = computed(() => ({
transform: `translate(${Number(rowEditorDialogPos.value?.x || 0)}px, ${Number(rowEditorDialogPos.value?.y || 0)}px)`
}))
function resetRowEditorDialogPosition () {
rowEditorDialogPos.value = { x: 0, y: 0 }
rowEditorDialogPanBase.value = { x: 0, y: 0 }
}
function startRowEditorDialogDrag (e) {
if (!e) return
rowEditorDialogDragging.value = true
rowEditorDialogDragMouseStart.value = { x: Number(e.clientX || 0), y: Number(e.clientY || 0) }
rowEditorDialogDragPosStart.value = { x: Number(rowEditorDialogPos.value?.x || 0), y: Number(rowEditorDialogPos.value?.y || 0) }
window.addEventListener('mousemove', onRowEditorDialogDragMove, true)
window.addEventListener('mouseup', stopRowEditorDialogDrag, true)
}
function onRowEditorDialogDragMove (e) {
if (!rowEditorDialogDragging.value) return
const startMouse = rowEditorDialogDragMouseStart.value || { x: 0, y: 0 }
const startPos = rowEditorDialogDragPosStart.value || { x: 0, y: 0 }
const dx = Number(e?.clientX || 0) - Number(startMouse.x || 0)
const dy = Number(e?.clientY || 0) - Number(startMouse.y || 0)
rowEditorDialogPos.value = { x: Number(startPos.x || 0) + dx, y: Number(startPos.y || 0) + dy }
}
function stopRowEditorDialogDrag () {
rowEditorDialogDragging.value = false
try {
window.removeEventListener('mousemove', onRowEditorDialogDragMove, true)
window.removeEventListener('mouseup', stopRowEditorDialogDrag, true)
} catch {}
}
const rowEditorHammaddeOptions = ref([]) const rowEditorHammaddeOptions = ref([])
const rowEditorHammaddeAllOptions = ref([]) const rowEditorHammaddeAllOptions = ref([])
const rowEditorHammaddeLoading = ref(false) const rowEditorHammaddeLoading = ref(false)
@@ -843,6 +906,13 @@ const hasUnsavedChanges = computed(() => {
return flatDetailRows.value.some(row => Boolean(row?.draftChanged)) return flatDetailRows.value.some(row => Boolean(row?.draftChanged))
}) })
const canDeleteCosting = computed(() => {
// Only allow delete for records created from no-cost flow.
// UX expectation: delete button becomes available after a costing record exists.
const n = parseInt(String(onMLNo.value || detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || '0'), 10) || 0
return n > 0 && String(detailSource.value || '').trim().toLowerCase() === 'no-cost'
})
function persistLocalDraftNow () { function persistLocalDraftNow () {
const key = draftStorageKey.value const key = draftStorageKey.value
if (!key) return if (!key) return
@@ -1796,6 +1866,19 @@ function resolveInputCurrency (row) {
return normalizePriceCurrency(row?.inputPricePrBr || row?.fiyat_doviz) || 'USD' return normalizePriceCurrency(row?.inputPricePrBr || row?.fiyat_doviz) || 'USD'
} }
function resolveAutoOrICodeHighlightClass (row) {
// Priority: auto-filled from previous costing (already firm-aware).
if (row?.__autoFilledFromPrev) {
return row?.__autoFilledFromPrevSameFirma ? 'text-positive text-weight-bold' : 'text-negative text-weight-bold'
}
// Manual selection for I.* codes: highlight same/other firm like the auto-fill convention.
const code = String(row?.sKodu || '').trim().toUpperCase()
if (code.startsWith('I.') && row?.__iCodeSelected) {
return row?.__iCodeSameFirma ? 'text-positive text-weight-bold' : 'text-negative text-weight-bold'
}
return ''
}
function resolveNumericRowQuantity (row) { function resolveNumericRowQuantity (row) {
return parseMoneyInput(row?.miktarInput ?? row?.lMiktar) return parseMoneyInput(row?.miktarInput ?? row?.lMiktar)
} }
@@ -1826,17 +1909,23 @@ function resolveUSDUnitPriceByInput (inputPrice, inputCurrency, usdRate, eurRate
function normalizeDetailRows (items, groupName = '') { function normalizeDetailRows (items, groupName = '') {
const list = Array.isArray(items) ? items : [] const list = Array.isArray(items) ? items : []
return list.map((x, i) => ({ return list.map((x, i) => {
// Determine the original currency from backend fields.
const originalCurrency = normalizePriceCurrency(x?.fiyat_doviz || x?.sDovizCinsi || x?.inputPricePrBr) || 'USD'
return {
...x, ...x,
__rowKey: x?.__rowKey || `${x?.nOnMLNo || ''}-${x?.nOnMLDetNo || ''}-${i}`, __rowKey: x?.__rowKey || `${x?.nOnMLNo || ''}-${x?.nOnMLDetNo || ''}-${i}`,
miktarInput: x?.miktarInput ?? normalizeQuantityInput(x?.lMiktar), miktarInput: x?.miktarInput ?? normalizeQuantityInput(x?.lMiktar),
// If inputPrice is missing, it means we are loading fresh from backend.
// Use fiyat_girilen which is the unit price in original currency.
inputPrice: x?.inputPrice ?? normalizeInputPrice(x?.fiyat_girilen), inputPrice: x?.inputPrice ?? normalizeInputPrice(x?.fiyat_girilen),
inputPricePrBr: normalizePriceCurrency(x?.inputPricePrBr || x?.fiyat_doviz || x?.sDovizCinsi) || 'USD', inputPricePrBr: originalCurrency,
maliyeteDahil: x?.maliyeteDahil ?? normalizeBooleanFlag(x?.maliyete_dahil ?? x?.Maliyete_dahil), maliyeteDahil: x?.maliyeteDahil ?? normalizeBooleanFlag(x?.maliyete_dahil ?? x?.Maliyete_dahil),
cmPriceTypeId: normalizeCMPriceTypeId(x?.cmPriceTypeId ?? x?.cm_price_type_id, groupName || x?.sAciklama3), cmPriceTypeId: normalizeCMPriceTypeId(x?.cmPriceTypeId ?? x?.cm_price_type_id, groupName || x?.sAciklama3),
draftChanged: Boolean(x?.draftChanged), draftChanged: Boolean(x?.draftChanged),
priceUpdateState: String(x?.priceUpdateState || '').trim() priceUpdateState: String(x?.priceUpdateState || '').trim()
})) }
})
} }
function normalizeDetailGroups (groups) { function normalizeDetailGroups (groups) {
@@ -2253,10 +2342,17 @@ function buildDetailItemRequestPayload (row) {
} }
} }
function applyPriceSelectionToRow (targetRowKey, price, currency, priceType) { function applyPriceSelectionToRow (targetRowKey, price, currency, priceType, itemCode, itemDescription, historyCompanyCode) {
const normalizedCurrency = normalizePriceCurrency(currency) || 'USD' const normalizedCurrency = normalizePriceCurrency(currency) || 'USD'
const normalizedPrice = parseMoneyInput(price) const normalizedPrice = parseMoneyInput(price)
const finalPriceType = priceType || 'SAF' const finalPriceType = priceType || 'SAF'
const normalizedCode = normalizeCodeValue(itemCode)
const normalizedDesc = String(itemDescription || '').trim()
const isICode = String(normalizedCode || '').toUpperCase().startsWith('I.')
const selectedFirma = String(detailHeader.value?.FirmaKodu || '').trim().toUpperCase()
const historyFirma = String(historyCompanyCode || '').trim().toUpperCase()
const isSameFirma = Boolean(selectedFirma && historyFirma && selectedFirma === historyFirma)
detailGroups.value = detailGroups.value.map(grp => ({ detailGroups.value = detailGroups.value.map(grp => ({
...grp, ...grp,
@@ -2264,6 +2360,9 @@ function applyPriceSelectionToRow (targetRowKey, price, currency, priceType) {
if (row.__rowKey !== targetRowKey) return row if (row.__rowKey !== targetRowKey) return row
return recalculateDetailRow({ return recalculateDetailRow({
...row, ...row,
...(normalizedCode ? { sKodu: normalizedCode } : {}),
...(normalizedDesc ? { sAciklama: normalizedDesc } : {}),
...(isICode ? { __iCodeSelected: true, __iCodeSameFirma: isSameFirma } : { __iCodeSelected: false, __iCodeSameFirma: false }),
inputPrice: normalizeInputPrice(normalizedPrice), inputPrice: normalizeInputPrice(normalizedPrice),
fiyat_girilen: normalizedPrice, fiyat_girilen: normalizedPrice,
inputPricePrBr: normalizedCurrency, inputPricePrBr: normalizedCurrency,
@@ -2481,6 +2580,94 @@ async function fetchBulkItemPrices () {
: 'Donen veriler satirlarla eslestirilemedi.', : 'Donen veriler satirlarla eslestirilemedi.',
position: 'top-right' position: 'top-right'
}) })
// Autofill missing required codes from last OnML history (URETIM spUrtOnMLMasDet)
try {
const flatNow = detailGroups.value.flatMap(grp => Array.isArray(grp?.items) ? grp.items : [])
const targets = flatNow.filter(r =>
Boolean(r?.requiredPlaceholder) &&
// CM1/CM2 and FABRIC rows must be chosen by user; don't auto-fill from previous costing.
!isCMGroupName(r?.sAciklama3) &&
normalizeGroupName(r?.sAciklama3) !== 'FABRIC' &&
(String(r?.sKodu || '').trim() === '' || Number(resolveNumericRowInputPrice(r) || 0) <= 0)
)
const hNos = Array.from(new Set(targets
.map(r => parseInt(String(r?.nHammaddeTuruNo || '').trim() || '0', 10))
.filter(n => n > 0)
))
if (hNos.length > 0) {
const lastRows = await post('/pricing/production-product-costing/has-cost-detail/last-detail', {
nHammaddeTuruNos: hNos,
before_date: normalizeDateInput(costDate.value),
exclude_onml_no: parseInt(String(onMLNo.value || detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || '0'), 10) || 0,
n_firma_id: parseInt(String(detailHeader.value?.nFirmaID || detailHeader.value?.NFirmaID || '0').trim() || '0', 10) || 0
}, { params: { trace_id: traceId.value } })
const list = Array.isArray(lastRows) ? lastRows : []
const byNo = {}
list.forEach(x => {
const no = parseInt(String(x?.nHammaddeTuruNo || '0'), 10) || 0
if (no > 0) byNo[no] = x
})
let filled = 0
detailGroups.value = detailGroups.value.map(grp => ({
...grp,
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => {
if (!row?.requiredPlaceholder) return row
if (isCMGroupName(row?.sAciklama3) || normalizeGroupName(row?.sAciklama3) === 'FABRIC') return row
const no = parseInt(String(row?.nHammaddeTuruNo || '').trim() || '0', 10) || 0
const hit = byNo[no]
if (!hit) return row
const next = { ...row }
const hasCode = String(next.sKodu || '').trim() !== ''
const hasPrice = Number(resolveNumericRowInputPrice(next) || 0) > 0
if (!hasCode && String(hit?.sKodu || '').trim()) {
next.sKodu = String(hit.sKodu || '').trim()
next.sAciklama = String(hit?.sAciklama || '').trim()
}
if (!hasPrice && Number(hit?.fiyat_girilen || 0) > 0) {
next.inputPrice = String(hit.fiyat_girilen)
next.fiyat_girilen = Number(hit.fiyat_girilen)
const pr = String(hit?.fiyat_doviz || '').trim() || 'USD'
next.inputPricePrBr = pr
next.fiyat_doviz = pr
}
// Only mark if something changed
const changed = (next.sKodu !== row.sKodu) || (next.sAciklama !== row.sAciklama) || (next.inputPrice !== row.inputPrice) || (next.inputPricePrBr !== row.inputPricePrBr)
if (!changed) return row
filled += 1
return recalculateDetailRow({
...next,
__autoFilledFromPrev: true
,
__autoFilledFromPrevSameFirma: Boolean(hit?.is_same_firma || hit?.isSameFirma)
}, {
priceType: 'PREV',
updateState: 'autofill',
markChanged: true
})
})
}))
if (filled > 0) {
$q.notify({
type: 'info',
message: `${filled} satirda kod/aciklama ve fiyat bilgisi onceki maliyetten otomatik getirildi.`,
position: 'top-right'
})
}
}
} catch (e) {
// Non-blocking
slog.error('production-product-costing.detail', 'bulk:autofill-prev:error', {
trace_id: traceId.value,
error: String(e?.message || e || '')
})
}
} catch (err) { } catch (err) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
@@ -2542,11 +2729,28 @@ function onDetailRowClick (evt, row) {
} }
function applyLineHistorySelection (historyRow) { function applyLineHistorySelection (historyRow) {
applyPriceSelectionToRow(lineHistoryTargetRowKey.value, historyRow?.price, historyRow?.currency, historyRow?.priceType) const targetKey = String(lineHistoryTargetRowKey.value || '').trim()
if (!targetKey) {
$q.notify({
type: 'negative',
message: 'Hedef satir bulunamadi (rowKey bos).',
position: 'top-right'
})
return
}
applyPriceSelectionToRow(
targetKey,
historyRow?.price,
historyRow?.currency,
historyRow?.priceType,
historyRow?.itemCode,
historyRow?.itemDescription,
historyRow?.companyCode
)
lineHistoryDialogOpen.value = false lineHistoryDialogOpen.value = false
$q.notify({ $q.notify({
type: 'positive', type: 'positive',
message: 'Secilen history fiyati satira uygulandi.', message: `Secilen fiyat satira uygulandi: ${formatMoney(historyRow?.price)} ${String(historyRow?.currency || '').trim() || 'USD'}`,
position: 'top-right' position: 'top-right'
}) })
} }
@@ -2759,8 +2963,10 @@ function normalizeGroupName (value) {
async function fetchRequiredParcaMappings () { async function fetchRequiredParcaMappings () {
const ilk = String(detailHeader.value?.UrunIlkGrubu || '').trim() 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() // Some sources return NULL/'' for "no alt group". Mapping screen stores it as '-'.
if (!ilk || !ana || !alt) return [] const altRaw = String(detailHeader.value?.UrunAltGrubu || '').trim()
const alt = altRaw || '-'
if (!ilk || !ana) 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,
@@ -2956,6 +3162,81 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
applyEditorRowToGroups(placeholder) applyEditorRowToGroups(placeholder)
} }
} }
// CM2 special case:
// Bulk autofill is disabled for CM2 (and FABRIC) to prevent wrong price pulls,
// but we still want the I.* code templates (code + description) to appear on page open.
// So we prefill ONLY code + description for required CM2 placeholders from last OnML history.
try {
const flatNow = detailGroups.value.flatMap(grp => Array.isArray(grp?.items) ? grp.items : [])
const cm2Targets = flatNow.filter(r =>
Boolean(r?.requiredPlaceholder) &&
normalizeGroupName(r?.sAciklama3) === 'CM2' &&
String(r?.sKodu || '').trim() === ''
)
const cm2Nos = Array.from(new Set(cm2Targets
.map(r => parseInt(String(r?.nHammaddeTuruNo || '').trim() || '0', 10))
.filter(n => n > 0)
))
if (cm2Nos.length > 0) {
const lastRows = await post('/pricing/production-product-costing/has-cost-detail/last-detail', {
nHammaddeTuruNos: cm2Nos,
before_date: normalizeDateInput(costDate.value),
exclude_onml_no: parseInt(String(onMLNo.value || detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || '0'), 10) || 0,
n_firma_id: parseInt(String(detailHeader.value?.nFirmaID || detailHeader.value?.NFirmaID || '0').trim() || '0', 10) || 0,
only_i_code: true
}, { params: { trace_id: traceId.value } })
const list = Array.isArray(lastRows) ? lastRows : []
const byNo = {}
list.forEach(x => {
const no = parseInt(String(x?.nHammaddeTuruNo || '0'), 10) || 0
if (no > 0) byNo[no] = x
})
let filled = 0
detailGroups.value = detailGroups.value.map(grp => ({
...grp,
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => {
if (!row?.requiredPlaceholder) return row
if (normalizeGroupName(row?.sAciklama3) !== 'CM2') return row
if (String(row?.sKodu || '').trim() !== '') return row
const no = parseInt(String(row?.nHammaddeTuruNo || '').trim() || '0', 10) || 0
const hit = byNo[no]
if (!hit) return row
const code = String(hit?.sKodu || '').trim()
if (!code) return row
const next = recalculateDetailRow({
...row,
sKodu: code,
sAciklama: String(hit?.sAciklama || '').trim(),
__autoFilledFromPrev: true,
__autoFilledFromPrevSameFirma: Boolean(hit?.is_same_firma || hit?.isSameFirma)
}, {
priceType: 'PREV',
updateState: 'autofill-icode',
markChanged: true
})
filled += 1
return next
})
}))
if (filled > 0) {
$q.notify({
type: 'info',
message: `${filled} CM2 satirinda I.* kod/aciklama onceki maliyetten otomatik getirildi.`,
position: 'top-right'
})
}
}
} catch (e) {
// Non-blocking
slog.error('production-product-costing.detail', 'cm2:autofill-icode:error', {
trace_id: traceId.value,
error: String(e?.message || e)
})
}
} }
function computeMissingRequiredSlots () { function computeMissingRequiredSlots () {
@@ -2964,19 +3245,30 @@ function computeMissingRequiredSlots () {
if (list.length === 0) return missing if (list.length === 0) return missing
list.forEach(mapping => { list.forEach(mapping => {
const groupName = normalizeGroupName(mapping?.parcaBolumAdi || mapping?.mtBolumAdi || mapping?.sAciklama3) // Required slots are defined per (ParcaBolum / MTBolumID) + (HammaddeTuruNo).
// Do NOT match by row.sAciklama3, because that field is the "group header" (DT/TP/CM2/FABRIC), not the part.
const mappingMtBolumID = parseInt(String(mapping?.nUrtMTBolumID ?? mapping?.NUrtMTBolumID ?? '0'), 10) || 0
const mappingParcaAdi = normalizeGroupName(mapping?.parcaBolumAdi || mapping?.mtBolumAdi || '')
const hList = Array.isArray(mapping?.nHammaddeTurleri) ? mapping.nHammaddeTurleri : [] const hList = Array.isArray(mapping?.nHammaddeTurleri) ? mapping.nHammaddeTurleri : []
hList.forEach(hNoRaw => { hList.forEach(hNoRaw => {
const hNo = normalizeHammaddeNo(hNoRaw) const hNo = normalizeHammaddeNo(hNoRaw)
if (!hNo) return if (!hNo) return
const match = flatDetailRows.value.find(r => const match = flatDetailRows.value.find(r => {
normalizeGroupName(r?.sAciklama3) === groupName && if (normalizeHammaddeNo(r?.nHammaddeTuruNo) !== hNo) return false
normalizeHammaddeNo(r?.nHammaddeTuruNo) === hNo const rowMtBolumID = parseInt(String(r?.nUrtMTBolumID ?? r?.NUrtMTBolumID ?? '0'), 10) || 0
) if (mappingMtBolumID > 0 && rowMtBolumID > 0) return mappingMtBolumID === rowMtBolumID
// Fallback: older rows might not have mtBolumID; use part name match.
return normalizeGroupName(r?.sParcaAdi) === mappingParcaAdi
})
const price = resolveNumericRowInputPrice(match) const price = resolveNumericRowInputPrice(match)
if (!match || !(price > 0)) { if (!match || !(price > 0)) {
missing.push({ groupName, nHammaddeTuruNo: hNo, rowKey: match?.__rowKey || '' }) missing.push({
mtBolumID: mappingMtBolumID,
parcaAdi: mappingParcaAdi,
nHammaddeTuruNo: hNo,
rowKey: match?.__rowKey || ''
})
} }
}) })
}) })
@@ -3131,6 +3423,16 @@ function round4 (n) {
return Math.round(x * 10000) / 10000 return Math.round(x * 10000) / 10000
} }
function escapeHtml (input) {
const s = String(input ?? '')
return s
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;')
}
async function confirmDefaultQtyDeviationIfNeeded () { async function confirmDefaultQtyDeviationIfNeeded () {
// Compare entered qty vs default qty (mk_MaliyetParcaEslestirme_vmiktarlar) per hammadde type. // Compare entered qty vs default qty (mk_MaliyetParcaEslestirme_vmiktarlar) per hammadde type.
// Rule: if deviation > 10% (abs), require user confirmation. // Rule: if deviation > 10% (abs), require user confirmation.
@@ -3154,10 +3456,15 @@ async function confirmDefaultQtyDeviationIfNeeded () {
return true return true
} }
const defMap = {} const defMap = {}
const descMap = {}
;(Array.isArray(defaults) ? defaults : []).forEach(it => { ;(Array.isArray(defaults) ? defaults : []).forEach(it => {
const no = parseInt(String(it?.nHammaddeTuruNo || '0'), 10) || 0 const no = parseInt(String(it?.nHammaddeTuruNo || '0'), 10) || 0
const d = Number(it?.lDefaultMiktar || 0) const d = Number(it?.lDefaultMiktar || 0)
if (no > 0 && d > 0) defMap[no] = d const desc = String(it?.sAciklama || '').trim()
if (no > 0 && d > 0) {
defMap[no] = d
if (desc) descMap[no] = desc
}
}) })
const outliers = [] const outliers = []
@@ -3176,18 +3483,53 @@ async function confirmDefaultQtyDeviationIfNeeded () {
outliers.sort((a, b) => Math.abs(b.pct) - Math.abs(a.pct)) outliers.sort((a, b) => Math.abs(b.pct) - Math.abs(a.pct))
const maxRows = 30 const maxRows = 30
const lines = outliers.slice(0, maxRows).map(x => { const rowsHtml = outliers.slice(0, maxRows).map(x => {
const sign = x.pct >= 0 ? '+' : '' const sign = x.pct >= 0 ? '+' : ''
return `${x.no}: varsayilan ${round4(x.defQty)} | girilen ${round4(x.enteredQty)} | fark ${sign}${round1(x.pct)}%` const pct = `${sign}${round1(x.pct)}%`
}) const cls = x.pct >= 0 ? 'color:#b71c1c;' : 'color:#1b5e20;'
const truncated = outliers.length > maxRows const desc = String(descMap[x.no] || '').trim()
? `\n... (Toplam ${outliers.length} satir. Ilk ${maxRows} gosterildi.)` const noLabel = desc ? `${x.no} - ${escapeHtml(desc)}` : String(x.no)
return `
<tr>
<td style="padding:6px 8px; white-space:nowrap; font-weight:600;">${noLabel}</td>
<td style="padding:6px 8px; text-align:right; white-space:nowrap;">${round4(x.defQty)}</td>
<td style="padding:6px 8px; text-align:right; white-space:nowrap;">${round4(x.enteredQty)}</td>
<td style="padding:6px 8px; text-align:right; white-space:nowrap; ${cls} font-weight:600;">${pct}</td>
</tr>
`
}).join('')
const truncatedNote = outliers.length > maxRows
? `<div style="margin-top:8px; color:#666;">Toplam ${outliers.length} satir var. Ilk ${maxRows} gosterildi.</div>`
: '' : ''
const ok = await new Promise(resolve => { const ok = await new Promise(resolve => {
$q.dialog({ $q.dialog({
title: 'Varsayilan Miktar Kontrolu', title: 'Varsayilan Miktar Kontrolu',
message: `Bazi hammadde turlerinde varsayilan miktardan %10'dan fazla sapma var.\n\n${lines.join('\n')}${truncated}\n\nOnayliyorsaniz Kaydet'e basın. Duzenlemek icin Geri Don.`, html: true,
message: `
<div style="margin-bottom:10px;">
Bazi hammadde turlerinde varsayilan miktardan <b>%10</b>'dan fazla sapma var.
</div>
<div style="max-height: 360px; overflow:auto; border:1px solid #e0e0e0; border-radius:6px;">
<table style="width:100%; border-collapse:collapse; font-size:13px;">
<thead>
<tr style="background:#f5f5f5; position: sticky; top: 0;">
<th style="text-align:left; padding:6px 8px;">Hammadde</th>
<th style="text-align:right; padding:6px 8px;">Varsayilan</th>
<th style="text-align:right; padding:6px 8px;">Girilen</th>
<th style="text-align:right; padding:6px 8px;">Fark %</th>
</tr>
</thead>
<tbody>
${rowsHtml}
</tbody>
</table>
</div>
${truncatedNote}
<div style="margin-top:10px;">
Onayliyorsaniz <b>Onayla ve Kaydet</b>'e basın. Duzenlemek icin <b>Geri Don</b>.
</div>
`,
cancel: { label: 'Geri Don' }, cancel: { label: 'Geri Don' },
ok: { label: 'Onayla ve Kaydet', color: 'primary' }, ok: { label: 'Onayla ve Kaydet', color: 'primary' },
persistent: true persistent: true
@@ -3196,6 +3538,57 @@ async function confirmDefaultQtyDeviationIfNeeded () {
return ok return ok
} }
async function deleteCosting () {
if (!detailHeader.value) return
const n = parseInt(String(detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || onMLNo.value || '0'), 10) || 0
if (!(n > 0)) {
$q.notify({ type: 'warning', message: 'Silinecek kayit bulunamadi.', position: 'top-right' })
return
}
const ok = await new Promise(resolve => {
$q.dialog({
title: 'Kaydi Sil',
message: `Bu maliyet kaydi silinecek. (OnMLNo=${n}) Devam edilsin mi?`,
cancel: true,
persistent: true
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
})
if (!ok) return
saveLoading.value = true
try {
await post('/pricing/production-product-costing/onml/delete', {
n_onml_no: n
})
clearLocalDraft()
$q.notify({ type: 'positive', message: 'Kayit silindi.', position: 'top-right' })
router.replace({ name: 'production-product-costing-no-cost' })
} catch (e) {
$q.notify({ type: 'negative', message: String(e?.message || e || 'Silinemedi'), position: 'top-right' })
} finally {
saveLoading.value = false
}
}
async function confirmRefresh () {
if (detailLoading.value) return
const ok = await new Promise(resolve => {
$q.dialog({
title: 'Yenile',
message: hasUnsavedChanges.value
? 'Bu islem sayfayi sifirlar ve kaydedilmemis degisiklikler kaybolur. Devam edilsin mi?'
: 'Bu islem sayfayi sifirlar. Devam edilsin mi?',
cancel: { label: 'Geri Don' },
ok: { label: 'Onayla', color: 'negative' },
persistent: true
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
})
if (!ok) return
clearLocalDraft()
await fetchDetail({ clearDraft: true, hydrateDraft: false })
}
async function saveChanges () { async function saveChanges () {
saveLoading.value = true saveLoading.value = true
try { try {
@@ -3206,7 +3599,7 @@ async function saveChanges () {
const ok = await new Promise(resolve => { const ok = await new Promise(resolve => {
$q.dialog({ $q.dialog({
title: 'Eksik Maliyet Parcalari', title: 'Eksik Maliyet Parcalari',
message: `Eslestirilen parcalarda (fiyat > 0) girilmemis satirlar var. Devam etmek istiyor musunuz? (Eksik: ${missing.length})`, message: `Eslestirilen parcalarda fiyat girilmemis satirlar var. Devam etmek istiyor musunuz? (Eksik: ${missing.length})`,
cancel: true, cancel: true,
persistent: true persistent: true
}).onOk(() => resolve(true)).onCancel(() => resolve(false)) }).onOk(() => resolve(true)).onCancel(() => resolve(false))
@@ -3250,7 +3643,8 @@ async function saveChanges () {
fiyat_girilen: Number(resolveNumericRowInputPrice(r) || 0), fiyat_girilen: Number(resolveNumericRowInputPrice(r) || 0),
fiyat_doviz: String(resolveInputCurrency(r) || '').trim(), fiyat_doviz: String(resolveInputCurrency(r) || '').trim(),
maliyete_dahil: (r?.maliyeteDahil || r?.maliyete_dahil) ? 1 : 0, maliyete_dahil: (r?.maliyeteDahil || r?.maliyete_dahil) ? 1 : 0,
cm_price_type_id: r?.cmPriceTypeId ?? r?.cm_price_type_id ?? null cm_price_type_id: r?.cmPriceTypeId ?? r?.cm_price_type_id ?? null,
s_aciklama3: String(r?.sAciklama3 || '').trim()
})) }))
const deletes = (Array.isArray(deletedDetailRows.value) ? deletedDetailRows.value : []).map(d => ({ const deletes = (Array.isArray(deletedDetailRows.value) ? deletedDetailRows.value : []).map(d => ({
@@ -3278,6 +3672,9 @@ async function saveChanges () {
$q.notify({ type: 'positive', message: 'Kaydedildi.', position: 'top-right' }) $q.notify({ type: 'positive', message: 'Kaydedildi.', position: 'top-right' })
// Force clear local draft before fetching fresh data to ensure we don't re-hydrate old inputs.
clearLocalDraft()
// If we created a new OnML (no-cost), switch to has-cost detail mode. // If we created a new OnML (no-cost), switch to has-cost detail mode.
if (isNoCostDetail.value && newOnMLNo > 0) { if (isNoCostDetail.value && newOnMLNo > 0) {
router.replace({ router.replace({
@@ -3289,6 +3686,25 @@ async function saveChanges () {
}) })
return return
} }
// For existing costing, just refresh the detail.
await fetchDetail({ clearDraft: true, hydrateDraft: false })
} catch (e) {
// Surface backend message (http.Error text) when available.
const msg = String(
e?.response?.data?.message ||
e?.response?.data ||
e?.message ||
e ||
'Kaydedilemedi'
)
slog.error('production-product-costing.detail', 'save:error', {
trace_id: traceId.value,
status: e?.response?.status,
error: msg
})
$q.notify({ type: 'negative', message: msg, position: 'top-right' })
return
} finally { } finally {
saveLoading.value = false saveLoading.value = false
} }
@@ -3489,6 +3905,11 @@ watch(
max-width: 96vw; max-width: 96vw;
} }
.pcd-row-editor-drag-handle {
cursor: move;
user-select: none;
}
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry .q-field__control) { .pcd-row-editor-dialog :deep(.pcd-row-editor-entry .q-field__control) {
background: color-mix(in srgb, var(--q-secondary) 18%, white) !important; background: color-mix(in srgb, var(--q-secondary) 18%, white) !important;
border: 1px solid var(--q-secondary) !important; border: 1px solid var(--q-secondary) !important;

View File

@@ -87,6 +87,43 @@
<template #top-left> <template #top-left>
<div class="row items-center q-gutter-sm"> <div class="row items-center q-gutter-sm">
<q-select
v-model="commonHammaddeSelected"
dense
filled
multiple
use-chips
clearable
emit-value
map-options
option-label="label"
option-value="value"
:options="commonHammaddeOptions"
:disable="loading || saving || commonHammaddeOptions.length === 0"
class="pcmm-multi-select"
style="min-width: 420px"
>
<template #selected-item="scope">
<q-chip
class="q-mr-xs"
dense
removable
@remove="scope.removeAtIndex(scope.index)"
>
{{ scope.opt.label }}
</q-chip>
</template>
<template #hint>
Ekrandaki satirlarda bulunan hammadde turleri (distinct)
</template>
</q-select>
<q-btn
color="negative"
icon="delete_sweep"
label="Butun Parcalardan Kaldir"
:disable="loading || saving || (commonHammaddeSelected || []).length === 0"
@click="confirmRemoveCommonHammadde"
/>
<q-btn <q-btn
label="Kolon Filtreleri" label="Kolon Filtreleri"
icon="filter_alt_off" icon="filter_alt_off"
@@ -327,6 +364,8 @@ const hammaddeLoading = ref(false)
const bolumByKey = ref({}) const bolumByKey = ref({})
const hammaddeByKey = ref({}) const hammaddeByKey = ref({})
const commonHammaddeSelected = ref([])
const hammaddeLabelCache = ref({}) // no -> "NO - ACIKLAMA"
const columns = [ const columns = [
{ name: 'copy_select', label: '', field: 'copy_select', align: 'center' }, { name: 'copy_select', label: '', field: 'copy_select', align: 'center' },
@@ -345,6 +384,22 @@ const canCopySelected = computed(() => copySelectedCount.value >= 2)
const saveSelectedCount = computed(() => Object.keys(saveSelectedKeyMap.value || {}).length) const saveSelectedCount = computed(() => Object.keys(saveSelectedKeyMap.value || {}).length)
const canSaveSelected = computed(() => saveSelectedCount.value > 0) const canSaveSelected = computed(() => saveSelectedCount.value > 0)
function findHammaddeLabel (no) {
const n = Number(no || 0)
if (!(n > 0)) return ''
const cached = String(hammaddeLabelCache.value?.[String(n)] || '').trim()
if (cached) return cached
const opt = (Array.isArray(hammaddeOptions.value) ? hammaddeOptions.value : []).find(o => Number(o?.value) === n)
const raw = String(opt?.label || '').trim()
if (raw) {
// Ensure "NO - ACIKLAMA" format. Some option sources may only provide the description.
if (/^\d+\s*-\s*/.test(raw)) return raw
if (/^\d+\s*$/.test(raw)) return `${n}`
return `${n} - ${raw}`
}
return `${n}`
}
function normalizeSearch (value) { function normalizeSearch (value) {
const s = String(value ?? '').trim() const s = String(value ?? '').trim()
if (!s) return '' if (!s) return ''
@@ -459,6 +514,33 @@ const rows = computed(() => {
return result return result
}) })
const commonHammaddeOptions = computed(() => {
const listRows = Array.isArray(rows.value) ? rows.value : []
if (listRows.length === 0) return []
const keys = listRows.map(r => String(r?.__key || '').trim()).filter(Boolean)
if (keys.length === 0) return []
// DISTINCT across visible rows (union).
const set = new Set()
for (const k of keys) {
const ham = normalizeIntList(hammaddeByKey.value?.[k] || [])
ham.forEach(n => { if (n > 0) set.add(n) })
}
return Array.from(set)
.sort((a, b) => a - b)
.map(n => ({ value: n, label: findHammaddeLabel(n) }))
})
watch(commonHammaddeOptions, (opts) => {
// Ensure labels for large/rare nos that won't be in the first 200 option fetch.
try {
const nos = (Array.isArray(opts) ? opts : []).map(o => Number(o?.value || 0)).filter(n => n > 0)
ensureHammaddeLabelsForNos(nos)
} catch {
// ignore
}
}, { immediate: true })
function hardResetAndRefresh () { function hardResetAndRefresh () {
// reset view state (filters + selections + dirty) // reset view state (filters + selections + dirty)
clearAllColumnFilters() clearAllColumnFilters()
@@ -626,9 +708,14 @@ function updateBolumSelection (key, newValue) {
function updateHammaddeSelection (key, newValue) { function updateHammaddeSelection (key, newValue) {
const k = String(key || '').trim() const k = String(key || '').trim()
if (!k) return if (!k) return
hammaddeByKey.value = { const nextList = normalizeIntList(newValue)
...(hammaddeByKey.value || {}), hammaddeByKey.value = { ...(hammaddeByKey.value || {}), [k]: nextList }
[k]: normalizeIntList(newValue) // Keep table field in sync for column filtering/sorting/export behavior.
const idx = (Array.isArray(mappings.value) ? mappings.value : []).findIndex(r => String(r?.__key || '') === k)
if (idx >= 0) {
const copy = [...mappings.value]
copy[idx] = { ...(copy[idx] || {}), nHammaddeTurleri: [...nextList] }
mappings.value = copy
} }
} }
@@ -653,6 +740,62 @@ function pruneHammaddeSelection (rowKey, list) {
return allowed return allowed
} }
function confirmRemoveCommonHammadde () {
const selected = normalizeIntList(commonHammaddeSelected.value || [])
if (selected.length === 0) return
const listRows = Array.isArray(rows.value) ? rows.value : []
const keys = listRows.map(r => String(r?.__key || '').trim()).filter(Boolean)
if (keys.length === 0) return
// Affected "parcalar": only rows that currently contain at least one selected hammadde.
const affectedRows = listRows.filter(r => {
const k = String(r?.__key || '').trim()
if (!k) return false
const current = normalizeIntList(hammaddeByKey.value?.[k] || [])
return current.some(v => selected.includes(v))
})
if (affectedRows.length === 0) {
$q.notify({ type: 'info', message: 'Secilen hammadde turleri bu ekrandaki satirlarda bulunmuyor.', position: 'top-right' })
return
}
const affected = affectedRows
.map(r => `${String(r?.urunIlkGrubu || '').trim() || '-'} | ${String(r?.urunAnaGrubu || '').trim() || '-'} | ${String(r?.urunAltGrubu || '').trim() || '-'}`)
.filter(Boolean)
const removedLabels = selected.map(n => findHammaddeLabel(n))
const htmlList = affected.slice(0, 30).map(x => `<div>${x}</div>`).join('')
const more = affected.length > 30 ? `<div class="text-grey-7 q-mt-sm">(+${affected.length - 30} satir daha)</div>` : ''
$q.dialog({
title: 'Butun Parcalardan Kaldir',
message: `
<div class="q-mb-sm"><b>Kaldirilacak hammadde turleri:</b></div>
<div class="q-mb-sm">${removedLabels.map(x => `<div>${x}</div>`).join('')}</div>
<div class="q-mb-sm"><b>Etkilenecek parcalar:</b></div>
<div style="max-height: 240px; overflow:auto; border: 1px solid #eee; padding: 8px;">${htmlList}${more}</div>
<div class="q-mt-sm text-grey-7">Onaylarsaniz secilen hammadde turleri sadece bu satirlardan kaldirilacak ve Degisenleri Kaydet ile kaydedilebilecek.</div>
`,
html: true,
cancel: true,
persistent: true,
ok: { label: 'Onayla', color: 'negative' },
cancelLabel: 'Geri Don'
}).onOk(() => {
const affectedKeys = affectedRows.map(r => String(r?.__key || '').trim()).filter(Boolean)
for (const k of affectedKeys) {
const current = normalizeIntList(hammaddeByKey.value?.[k] || [])
const next = current.filter(v => !selected.includes(v))
if (String(next) === String(current)) continue
updateHammaddeSelection(k, next)
const row = listRows.find(r => String(r?.__key || '') === k) || (Array.isArray(mappings.value) ? mappings.value : []).find(r => String(r?.__key || '') === k)
if (row) markDirty(row)
}
commonHammaddeSelected.value = []
$q.notify({ type: 'positive', message: 'Secilen hammadde turleri etkilenen satirlardan kaldirildi (taslak).', position: 'top-right' })
})
}
// 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
@@ -816,11 +959,55 @@ async function fetchHammaddeOptions (search) {
.filter(x => Number.isFinite(x.value) && x.value > 0) .filter(x => Number.isFinite(x.value) && x.value > 0)
.sort((a, b) => a.value - b.value) .sort((a, b) => a.value - b.value)
: [] : []
// Prime cache with currently loaded options.
try {
const next = { ...(hammaddeLabelCache.value || {}) }
;(Array.isArray(hammaddeOptions.value) ? hammaddeOptions.value : []).forEach(opt => {
const no = Number(opt?.value || 0)
const label = String(opt?.label || '').trim()
if (no > 0 && label) next[String(no)] = label
})
hammaddeLabelCache.value = next
} catch {
// ignore
}
} finally { } finally {
hammaddeLoading.value = false hammaddeLoading.value = false
} }
} }
async function ensureHammaddeLabelsForNos (nos) {
const list = normalizeIntList(nos || [])
if (list.length === 0) return
const missing = list.filter(n => {
const cached = String(hammaddeLabelCache.value?.[String(n)] || '').trim()
if (cached) return false
const opt = (Array.isArray(hammaddeOptions.value) ? hammaddeOptions.value : []).find(o => Number(o?.value) === n)
return !String(opt?.label || '').trim()
})
if (missing.length === 0) return
try {
const data = await post('/pricing/production-product-costing/options/hammadde-by-nos', {
nHammaddeTuruNos: missing
}, { trace_id: traceId })
const rows = Array.isArray(data) ? data : []
const next = { ...(hammaddeLabelCache.value || {}) }
rows.forEach(r => {
const no = Number(r?.nHammaddeTuruNo || 0)
const name = String(r?.sAciklama || '').trim()
if (no > 0 && name) next[String(no)] = `${no} - ${name}`
})
hammaddeLabelCache.value = next
} catch (e) {
// Non-blocking: fallback will show only the number
slog.error('production-product-costing.mtbolum-map', 'hammadde-by-nos:error', {
trace_id: traceId,
detail: await extractApiErrorDetail(e)
})
}
}
function onFilterMTBolum (val, update) { function onFilterMTBolum (val, update) {
update(async () => { update(async () => {
await fetchMTBolumOptions(val) await fetchMTBolumOptions(val)

View File

@@ -18,7 +18,7 @@ export const useProductionProductCostingDefaultQtyStore = defineStore('productio
const saving = ref(false) const saving = ref(false)
const error = ref('') const error = ref('')
// draftByNo: { [nHammaddeTuruNo]: { lDefaultMiktar, bAktif } } // draftByNo: { [nHammaddeTuruNo]: { lDefaultMiktar } }
const draftByNo = ref({}) const draftByNo = ref({})
const persistTimer = ref(0) const persistTimer = ref(0)
@@ -94,12 +94,11 @@ export const useProductionProductCostingDefaultQtyStore = defineStore('productio
return { return {
...row, ...row,
lDefaultMiktar: typeof draft.lDefaultMiktar === 'number' ? draft.lDefaultMiktar : row?.lDefaultMiktar, lDefaultMiktar: typeof draft.lDefaultMiktar === 'number' ? draft.lDefaultMiktar : row?.lDefaultMiktar,
bAktif: typeof draft.bAktif === 'boolean' ? draft.bAktif : row?.bAktif,
__dirty: true __dirty: true
} }
} }
async function fetch ({ search = '', onlyActive = true, limit = 2000 } = {}) { async function fetch ({ search = '', limit = 2000 } = {}) {
loading.value = true loading.value = true
error.value = '' error.value = ''
try { try {
@@ -136,8 +135,7 @@ export const useProductionProductCostingDefaultQtyStore = defineStore('productio
const patch = draftByNo.value?.[String(no)] || {} const patch = draftByNo.value?.[String(no)] || {}
return { return {
nHammaddeTuruNo: Number(no), nHammaddeTuruNo: Number(no),
lDefaultMiktar: Number(patch.lDefaultMiktar || 0), lDefaultMiktar: Number(patch.lDefaultMiktar || 0)
bAktif: typeof patch.bAktif === 'boolean' ? patch.bAktif : undefined
} }
}).filter(it => it.nHammaddeTuruNo > 0 && it.lDefaultMiktar > 0) }).filter(it => it.nHammaddeTuruNo > 0 && it.lDefaultMiktar > 0)