diff --git a/svc/db/migrations/2026-06-19_order_price_list_user_price_groups.sql b/svc/db/migrations/2026-06-19_order_price_list_user_price_groups.sql new file mode 100644 index 0000000..e6ffdc8 --- /dev/null +++ b/svc/db/migrations/2026-06-19_order_price_list_user_price_groups.sql @@ -0,0 +1,21 @@ +CREATE TABLE IF NOT EXISTS mk_order_price_list_user_price_group ( + user_id BIGINT NOT NULL, + price_group TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (user_id, price_group) +); + +CREATE INDEX IF NOT EXISTS ix_order_price_list_user_price_group_user + ON mk_order_price_list_user_price_group (user_id); + +ALTER TABLE mk_order_price_list_user_price_group + DROP CONSTRAINT IF EXISTS ck_order_price_list_user_price_group; + +ALTER TABLE mk_order_price_list_user_price_group + ADD CONSTRAINT ck_order_price_list_user_price_group + CHECK (price_group IN ( + 'usd1','usd2','usd3','usd4','usd5','usd6', + 'eur1','eur2','eur3','eur4','eur5','eur6', + 'try1','try2','try3','try4','try5','try6' + )); diff --git a/svc/main.go b/svc/main.go index b1886c6..64d1365 100644 --- a/svc/main.go +++ b/svc/main.go @@ -375,6 +375,21 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "system", "update", wrapV3(routes.SaveOrderPriceListFirstGroupMailMappingHandler(pgDB)), ) + bindV3(r, pgDB, + "/api/system/order-price-list-user-price-groups/lookups", "GET", + "system", "update", + wrapV3(routes.GetOrderPriceListPriceGroupLookupsHandler(pgDB)), + ) + bindV3(r, pgDB, + "/api/system/order-price-list-user-price-groups", "GET", + "system", "update", + wrapV3(routes.GetOrderPriceListUserPriceGroupRowsHandler(pgDB)), + ) + bindV3(r, pgDB, + "/api/system/order-price-list-user-price-groups/{id}", "PUT", + "system", "update", + wrapV3(routes.SaveUserOrderPriceListPriceGroupsHandler(pgDB)), + ) bindV3(r, pgDB, "/api/language/translations", "GET", "language", "update", @@ -439,6 +454,21 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "system", "update", wrapV3(routes.SaveUserPermissionsHandler(pgDB)), ) + bindV3(r, pgDB, + "/api/users/{id}/order-price-list-price-groups", "GET", + "user", "update", + wrapV3(routes.GetUserOrderPriceListPriceGroupsHandler(pgDB)), + ) + bindV3(r, pgDB, + "/api/users/order-price-list-price-groups/lookups", "GET", + "user", "update", + wrapV3(routes.GetOrderPriceListPriceGroupLookupsHandler(pgDB)), + ) + bindV3(r, pgDB, + "/api/users/{id}/order-price-list-price-groups", "PUT", + "user", "update", + wrapV3(routes.SaveUserOrderPriceListPriceGroupsHandler(pgDB)), + ) // ✅ permissions/routes (system:view) bindV3(r, pgDB, @@ -810,6 +840,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "order", "view", wrapV3(http.HandlerFunc(routes.GetProductPricingFilterOptionsHandler)), ) + bindV3(r, pgDB, + "/api/order/price-list/my-price-groups", "GET", + "order", "view", + wrapV3(routes.GetMyOrderPriceListPriceGroupsHandler(pgDB)), + ) bindV3(r, pgDB, "/api/order/price-list/campaigns", "GET", "order", "view", diff --git a/svc/models/user_detail.go b/svc/models/user_detail.go index 8e83a67..926cb5b 100644 --- a/svc/models/user_detail.go +++ b/svc/models/user_detail.go @@ -18,10 +18,11 @@ type UserDetail struct { HasPassword bool `json:"has_password"` // 🔐 SADECE DURUM // ===== İLİŞKİLER ===== - Roles []string `json:"roles"` - Departments []DeptOption `json:"departments"` - Piyasalar []DeptOption `json:"piyasalar"` - NebimUsers []NebimOption `json:"nebim_users"` + Roles []string `json:"roles"` + Departments []DeptOption `json:"departments"` + Piyasalar []DeptOption `json:"piyasalar"` + NebimUsers []NebimOption `json:"nebim_users"` + OrderPriceListPriceGroups []string `json:"order_price_list_price_groups"` } // ====================================================== @@ -35,10 +36,11 @@ type UserWrite struct { Mobile string `json:"mobile"` Address string `json:"address"` - Roles []string `json:"roles"` - Departments []DeptOption `json:"departments"` - Piyasalar []DeptOption `json:"piyasalar"` - NebimUsers []NebimOption `json:"nebim_users"` + Roles []string `json:"roles"` + Departments []DeptOption `json:"departments"` + Piyasalar []DeptOption `json:"piyasalar"` + NebimUsers []NebimOption `json:"nebim_users"` + OrderPriceListPriceGroups []string `json:"order_price_list_price_groups"` } // ====================================================== diff --git a/svc/routes/login.go b/svc/routes/login.go index da11915..8d41920 100644 --- a/svc/routes/login.go +++ b/svc/routes/login.go @@ -555,6 +555,17 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc { } } + if err := ensureOrderPriceListUserPriceGroupSchema(db); err != nil { + log.Printf("USER CREATE PRICE GROUP SCHEMA ERROR user_id=%d err=%v", newID, err) + http.Error(w, "Fiyat grubu tablosu hazirlanamadi", http.StatusInternalServerError) + return + } + if err := saveOrderPriceListUserPriceGroupsTx(tx, newID, payload.OrderPriceListPriceGroups); err != nil { + log.Printf("USER CREATE PRICE GROUP SAVE ERROR user_id=%d err=%v", newID, err) + http.Error(w, "Fiyat gruplari eklenemedi", http.StatusInternalServerError) + return + } + if err := tx.Commit(); err != nil { if pe, ok := err.(*pq.Error); ok { log.Printf( diff --git a/svc/routes/order_price_list_user_price_groups.go b/svc/routes/order_price_list_user_price_groups.go new file mode 100644 index 0000000..5fa28eb --- /dev/null +++ b/svc/routes/order_price_list_user_price_groups.go @@ -0,0 +1,278 @@ +package routes + +import ( + "bssapp-backend/auth" + "database/sql" + "encoding/json" + "log" + "net/http" + "strconv" + "strings" + "time" + + "github.com/gorilla/mux" + "github.com/lib/pq" +) + +type orderPriceListPriceGroupOption struct { + Value string `json:"value"` + Label string `json:"label"` +} + +type orderPriceListUserPriceGroupRow struct { + UserID int64 `json:"user_id"` + Username string `json:"username"` + FullName string `json:"full_name"` + Email string `json:"email"` + PriceGroups []string `json:"price_groups"` +} + +type orderPriceListUserPriceGroupPayload struct { + PriceGroups []string `json:"price_groups"` +} + +var orderPriceListAllPriceGroups = []orderPriceListPriceGroupOption{ + {Value: "usd1", Label: "USD 1"}, + {Value: "usd2", Label: "USD 2"}, + {Value: "usd3", Label: "USD 3"}, + {Value: "usd4", Label: "USD 4"}, + {Value: "usd5", Label: "USD 5"}, + {Value: "usd6", Label: "USD 6"}, + {Value: "eur1", Label: "EUR 1"}, + {Value: "eur2", Label: "EUR 2"}, + {Value: "eur3", Label: "EUR 3"}, + {Value: "eur4", Label: "EUR 4"}, + {Value: "eur5", Label: "EUR 5"}, + {Value: "eur6", Label: "EUR 6"}, + {Value: "try1", Label: "TRY 1"}, + {Value: "try2", Label: "TRY 2"}, + {Value: "try3", Label: "TRY 3"}, + {Value: "try4", Label: "TRY 4"}, + {Value: "try5", Label: "TRY 5"}, + {Value: "try6", Label: "TRY 6"}, +} + +func ensureOrderPriceListUserPriceGroupSchema(db *sql.DB) error { + stmts := []string{ + `CREATE TABLE IF NOT EXISTS mk_order_price_list_user_price_group ( + user_id BIGINT NOT NULL, + price_group TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (user_id, price_group) + )`, + `CREATE INDEX IF NOT EXISTS ix_order_price_list_user_price_group_user ON mk_order_price_list_user_price_group (user_id)`, + `ALTER TABLE mk_order_price_list_user_price_group DROP CONSTRAINT IF EXISTS ck_order_price_list_user_price_group`, + `ALTER TABLE mk_order_price_list_user_price_group ADD CONSTRAINT ck_order_price_list_user_price_group CHECK (price_group IN ('usd1','usd2','usd3','usd4','usd5','usd6','eur1','eur2','eur3','eur4','eur5','eur6','try1','try2','try3','try4','try5','try6'))`, + } + for _, stmt := range stmts { + if _, err := db.Exec(stmt); err != nil { + return err + } + } + return nil +} + +func normalizeOrderPriceListPriceGroups(groups []string) []string { + allowed := map[string]bool{} + order := []string{} + for _, opt := range orderPriceListAllPriceGroups { + allowed[opt.Value] = true + order = append(order, opt.Value) + } + set := map[string]bool{} + for _, item := range groups { + v := strings.ToLower(strings.TrimSpace(item)) + if allowed[v] { + set[v] = true + } + } + out := make([]string, 0, len(set)) + for _, v := range order { + if set[v] { + out = append(out, v) + } + } + return out +} + +func loadOrderPriceListUserPriceGroups(db *sql.DB, userID int64) ([]string, error) { + if err := ensureOrderPriceListUserPriceGroupSchema(db); err != nil { + return nil, err + } + rows, err := db.Query(` + SELECT price_group + FROM mk_order_price_list_user_price_group + WHERE user_id = $1 + ORDER BY + CASE SUBSTRING(price_group, 1, 3) + WHEN 'usd' THEN 1 + WHEN 'eur' THEN 2 + WHEN 'try' THEN 3 + ELSE 9 + END, + CAST(SUBSTRING(price_group, 4) AS INT) + `, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var out []string + for rows.Next() { + var group string + if err := rows.Scan(&group); err != nil { + return nil, err + } + out = append(out, strings.ToLower(strings.TrimSpace(group))) + } + return normalizeOrderPriceListPriceGroups(out), rows.Err() +} + +func saveOrderPriceListUserPriceGroupsTx(tx *sql.Tx, userID int64, groups []string) error { + if _, err := tx.Exec(`DELETE FROM mk_order_price_list_user_price_group WHERE user_id = $1`, userID); err != nil { + return err + } + for _, group := range normalizeOrderPriceListPriceGroups(groups) { + if _, err := tx.Exec(` + INSERT INTO mk_order_price_list_user_price_group (user_id, price_group, created_at, updated_at) + VALUES ($1, $2, $3, $3) + ON CONFLICT (user_id, price_group) + DO UPDATE SET updated_at = EXCLUDED.updated_at + `, userID, group, time.Now()); err != nil { + return err + } + } + return nil +} + +func GetOrderPriceListPriceGroupLookupsHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if err := ensureOrderPriceListUserPriceGroupSchema(db); err != nil { + log.Printf("[order-price-list-price-groups] schema error: %v", err) + http.Error(w, "price group schema error", http.StatusInternalServerError) + return + } + _ = json.NewEncoder(w).Encode(map[string]any{"price_groups": orderPriceListAllPriceGroups}) + } +} + +func GetMyOrderPriceListPriceGroupsHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + groups, err := loadOrderPriceListUserPriceGroups(db, int64(claims.ID)) + if err != nil { + log.Printf("[order-price-list-price-groups] my groups error user=%d err=%v", claims.ID, err) + http.Error(w, "price groups lookup error", http.StatusInternalServerError) + return + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "price_groups": groups, + "restricted": len(groups) > 0, + "all_groups": orderPriceListAllPriceGroups, + }) + } +} + +func GetUserOrderPriceListPriceGroupsHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + id, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64) + if err != nil || id <= 0 { + http.Error(w, "invalid user id", http.StatusBadRequest) + return + } + groups, err := loadOrderPriceListUserPriceGroups(db, id) + if err != nil { + log.Printf("[order-price-list-price-groups] user groups error user=%d err=%v", id, err) + http.Error(w, "price groups lookup error", http.StatusInternalServerError) + return + } + _ = json.NewEncoder(w).Encode(map[string]any{"price_groups": groups}) + } +} + +func SaveUserOrderPriceListPriceGroupsHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + id, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64) + if err != nil || id <= 0 { + http.Error(w, "invalid user id", http.StatusBadRequest) + return + } + var payload orderPriceListUserPriceGroupPayload + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, "invalid payload", http.StatusBadRequest) + return + } + if err := ensureOrderPriceListUserPriceGroupSchema(db); err != nil { + http.Error(w, "price group schema error", http.StatusInternalServerError) + return + } + tx, err := db.Begin() + if err != nil { + http.Error(w, "transaction error", http.StatusInternalServerError) + return + } + defer tx.Rollback() + if err := saveOrderPriceListUserPriceGroupsTx(tx, id, payload.PriceGroups); err != nil { + log.Printf("[order-price-list-price-groups] save error user=%d err=%v", id, err) + http.Error(w, "price groups save error", http.StatusInternalServerError) + return + } + if err := tx.Commit(); err != nil { + http.Error(w, "commit error", http.StatusInternalServerError) + return + } + _ = json.NewEncoder(w).Encode(map[string]any{"success": true}) + } +} + +func GetOrderPriceListUserPriceGroupRowsHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if err := ensureOrderPriceListUserPriceGroupSchema(db); err != nil { + http.Error(w, "price group schema error", http.StatusInternalServerError) + return + } + rows, err := db.Query(` + SELECT u.id, u.username, COALESCE(u.full_name, ''), COALESCE(u.email, ''), + COALESCE(array_agg(m.price_group ORDER BY + CASE SUBSTRING(m.price_group, 1, 3) + WHEN 'usd' THEN 1 + WHEN 'eur' THEN 2 + WHEN 'try' THEN 3 + ELSE 9 + END, + CAST(SUBSTRING(m.price_group, 4) AS INT) + ) FILTER (WHERE m.price_group IS NOT NULL), ARRAY[]::text[]) AS price_groups + FROM mk_dfusr u + LEFT JOIN mk_order_price_list_user_price_group m ON m.user_id = u.id + WHERE COALESCE(u.is_active, TRUE) = TRUE + GROUP BY u.id, u.username, u.full_name, u.email + ORDER BY u.username + `) + if err != nil { + log.Printf("[order-price-list-price-groups] rows error: %v", err) + http.Error(w, "price groups rows error", http.StatusInternalServerError) + return + } + defer rows.Close() + out := []orderPriceListUserPriceGroupRow{} + for rows.Next() { + var row orderPriceListUserPriceGroupRow + if err := rows.Scan(&row.UserID, &row.Username, &row.FullName, &row.Email, pq.Array(&row.PriceGroups)); err != nil { + http.Error(w, "price groups scan error", http.StatusInternalServerError) + return + } + row.PriceGroups = normalizeOrderPriceListPriceGroups(row.PriceGroups) + out = append(out, row) + } + _ = json.NewEncoder(w).Encode(out) + } +} diff --git a/svc/routes/user_detail.go b/svc/routes/user_detail.go index 23de038..630989e 100644 --- a/svc/routes/user_detail.go +++ b/svc/routes/user_detail.go @@ -184,6 +184,12 @@ func handleUserGet(db *sql.DB, w http.ResponseWriter, userID int64) { } } + if groups, err := loadOrderPriceListUserPriceGroups(db, userID); err == nil { + u.OrderPriceListPriceGroups = groups + } else { + log.Printf("WARN [UserDetail] order price list price groups lookup failed user_id=%d err=%v", userID, err) + } + // -------------------------------------------------- // 🟢 RESPONSE // -------------------------------------------------- @@ -326,6 +332,17 @@ func handleUserUpdate(db *sql.DB, w http.ResponseWriter, r *http.Request, userID } } + if err := ensureOrderPriceListUserPriceGroupSchema(db); err != nil { + log.Printf("ERROR [UserDetail] price group schema failed user_id=%d err=%v", userID, err) + http.Error(w, "Fiyat grubu tablosu hazirlanamadi", http.StatusInternalServerError) + return + } + if err := saveOrderPriceListUserPriceGroupsTx(tx, userID, payload.OrderPriceListPriceGroups); err != nil { + log.Printf("ERROR [UserDetail] price groups save failed user_id=%d err=%v", userID, err) + http.Error(w, "Fiyat gruplari guncellenemedi", http.StatusInternalServerError) + return + } + if err := tx.Commit(); err != nil { log.Printf("❌ [UserDetail] commit failed user_id=%d err=%v", userID, err) http.Error(w, "Commit başarısız", http.StatusInternalServerError) @@ -384,6 +401,7 @@ func handleUserDelete(db *sql.DB, w http.ResponseWriter, r *http.Request, userID `DELETE FROM dfusr_dprt WHERE dfusr_id = $1`, `DELETE FROM dfusr_piyasa WHERE dfusr_id = $1`, `DELETE FROM dfusr_nebim_user WHERE dfusr_id = $1`, + `DELETE FROM mk_order_price_list_user_price_group WHERE user_id = $1`, } isUndefinedTable := func(err error) bool { diff --git a/ui/src/layouts/MainLayout.vue b/ui/src/layouts/MainLayout.vue index 6a79a16..e679990 100644 --- a/ui/src/layouts/MainLayout.vue +++ b/ui/src/layouts/MainLayout.vue @@ -418,6 +418,11 @@ const menuItems = [ label: 'Fiyat Listesi Mail Eşleştirme', to: '/app/order-price-list-mail-mapping', permission: 'system:update' + }, + { + label: 'Kullanıcı Fiyat Eşleştirme', + to: '/app/order-price-list-user-price-groups', + permission: 'system:update' } ] diff --git a/ui/src/pages/OrderPriceList.vue b/ui/src/pages/OrderPriceList.vue index 84caea9..aa5d123 100644 --- a/ui/src/pages/OrderPriceList.vue +++ b/ui/src/pages/OrderPriceList.vue @@ -80,7 +80,7 @@ Tumunu Temizle - + Kampanya{{ productCardData.campaignLabel || '-' }} Stok{{ formatStock(productCardData.stockQty || 0) }} - - Fiyat Bilgileri - - - {{ item.label }} - {{ item.price || '-' }} - {{ item.campaignPrice || '-' }} - - - Secili fiyat kolonu yok. - - Beden Stoklari @@ -615,6 +603,25 @@ Beden stogu bulunamadi. + + + + Fiyat Bilgileri + + Fiyat + Liste + Kampanyali + + + + {{ item.label }} + {{ item.price || '-' }} + {{ item.campaignPrice || '-' }} + + + Secili fiyat kolonu yok. + + @@ -664,10 +671,18 @@ import api from 'src/services/api' const PAGE_LIMIT = 250 const GUIDANCE_MSG = 'Liste icin filtre secin.' -const priceOptions = ['USD', 'EUR', 'TRY'].flatMap((cur) => [1, 2, 3, 4, 5, 6].map((lv) => ({ +const allPriceOptions = ['USD', 'EUR', 'TRY'].flatMap((cur) => [1, 2, 3, 4, 5, 6].map((lv) => ({ label: `${cur} ${lv}`, value: `${cur.toLowerCase()}${lv}` }))) +const allowedPriceGroupValues = ref([]) +const priceGroupRestricted = ref(false) +const allowedPriceOptions = computed(() => { + if (!priceGroupRestricted.value) return allPriceOptions + const allowed = new Set(allowedPriceGroupValues.value || []) + return allPriceOptions.filter((x) => allowed.has(x.value)) +}) +const priceOptions = allPriceOptions const campaignPairs = priceOptions.map((x) => ({ base: x.value, derived: `${x.value}Campaign` })) const priceColumnNames = campaignPairs.flatMap((p) => [p.base, p.derived]) @@ -721,8 +736,10 @@ const productCardPriceRows = computed(() => { .map((option) => ({ key: option.value, label: option.label, + currency: String(option.value || '').slice(0, 3).toLowerCase(), price: formatPrice(row?.[option.value]), - campaignPrice: formatPrice(row?.[`${option.value}Campaign`]) + campaignPrice: formatPrice(row?.[`${option.value}Campaign`]), + hasCampaignPrice: Number(row?.[`${option.value}Campaign`] || 0) > 0 })) }) const selectedProductCodeSet = computed(() => new Set(selectedProductCodes.value || [])) @@ -956,6 +973,20 @@ async function fetchServerFilterOptions (field, q = '') { } } +async function fetchMyPriceGroups () { + try { + const res = await api.get('/order/price-list/my-price-groups') + priceGroupRestricted.value = !!res?.data?.restricted + allowedPriceGroupValues.value = Array.isArray(res?.data?.price_groups) ? res.data.price_groups : [] + normalizeSelectedPriceOptions() + } catch (err) { + console.warn('[order-price-list][ui] price-groups lookup failed', err?.response?.data || err?.message || err) + priceGroupRestricted.value = false + allowedPriceGroupValues.value = [] + normalizeSelectedPriceOptions() + } +} + function onTopFilterSearchUrunIlkGrubu (val, update) { update(() => { filterSearch.value.urunIlkGrubu = toText(val) @@ -1306,20 +1337,33 @@ function onPageChange (page) { } function togglePriceOption (value) { + if (!allowedPriceOptions.value.some((x) => x.value === value)) return const set = new Set(selectedPriceOptions.value || []) if (set.has(value)) set.delete(value) else set.add(value) - selectedPriceOptions.value = priceOptions.map((x) => x.value).filter((x) => set.has(x)) + selectedPriceOptions.value = allowedPriceOptions.value.map((x) => x.value).filter((x) => set.has(x)) } function selectAllPrices () { - selectedPriceOptions.value = priceOptions.map((x) => x.value) + selectedPriceOptions.value = allowedPriceOptions.value.map((x) => x.value) } function clearAllPrices () { selectedPriceOptions.value = [] } +function normalizeSelectedPriceOptions () { + const allowedValues = allowedPriceOptions.value.map((x) => x.value) + const allowed = new Set(allowedValues) + const current = (selectedPriceOptions.value || []).filter((x) => allowed.has(x)) + if (current.length > 0 || allowedValues.length === 0) { + selectedPriceOptions.value = current + return + } + const preferred = ['usd5', 'try5'].filter((x) => allowed.has(x)) + selectedPriceOptions.value = preferred.length ? preferred : allowedValues.slice(0, 2) +} + function col (name, label, field, width, extra = {}) { return { name, @@ -1402,6 +1446,7 @@ const filteredRows = computed(() => { return list }) const tableMinWidth = computed(() => visibleColumns.value.reduce((sum, c) => sum + extractWidth(c.style), 0)) +const tableScrollWidth = computed(() => tableMinWidth.value + stickyScrollComp.value + 48) const tableStyle = computed(() => ({ width: `${tableMinWidth.value}px`, minWidth: `${tableMinWidth.value}px`, @@ -1609,7 +1654,12 @@ watch([tableMinWidth, rows], async () => { bindTableScrollSync() }) +watch(allowedPriceOptions, () => { + normalizeSelectedPriceOptions() +}) + onMounted(() => { + void fetchMyPriceGroups() void fetchServerFilterOptions('urunIlkGrubu', '') void fetchServerFilterOptions('urunAnaGrubu', '') void fetchServerFilterOptions('productCode', '') @@ -1885,23 +1935,23 @@ onMounted(() => { .pricing-table :deep(th.usd-col), .pricing-table :deep(td.usd-col) { - background: #ecf9f0; - color: #178a3e; - font-weight: 700; + background: #fff; + color: #16803a; + font-weight: 800; } .pricing-table :deep(th.eur-col), .pricing-table :deep(td.eur-col) { - background: #fdeeee; - color: #c62828; - font-weight: 700; + background: #fff; + color: #b91c1c; + font-weight: 800; } .pricing-table :deep(th.try-col), .pricing-table :deep(td.try-col) { - background: #edf4ff; - color: #1e63c6; - font-weight: 700; + background: #fff; + color: #185abc; + font-weight: 800; } .pricing-table :deep(th.usd-col), @@ -1910,14 +1960,30 @@ onMounted(() => { .pricing-table :deep(td.usd-col), .pricing-table :deep(td.eur-col), .pricing-table :deep(td.try-col) { - font-size: 10px; + font-size: 12px; } -.pricing-table :deep(td.campaign-price-col), -.pricing-table :deep(th.campaign-price-col) { - background: #fff3f1; - color: #c62828; - font-weight: 800; +.pricing-table :deep(th.usd-col.campaign-price-col), +.pricing-table :deep(td.usd-col.campaign-price-col) { + background: #dff6e7; + color: #0f6b2f; + font-weight: 900; + letter-spacing: 0; +} + +.pricing-table :deep(th.eur-col.campaign-price-col), +.pricing-table :deep(td.eur-col.campaign-price-col) { + background: #fde2e2; + color: #a61717; + font-weight: 900; + letter-spacing: 0; +} + +.pricing-table :deep(th.try-col.campaign-price-col), +.pricing-table :deep(td.try-col.campaign-price-col) { + background: #e2edff; + color: #174ea6; + font-weight: 900; letter-spacing: 0; } @@ -1989,7 +2055,6 @@ onMounted(() => { } .campaign-price-text { - color: #c62828; font-weight: 900; } @@ -2100,7 +2165,7 @@ onMounted(() => { .product-card-dialog { --pc-media-h: calc(100vh - 180px); - --pc-media-w: min(74vw, 1220px); + --pc-media-w: min(28vw, 440px); background: #f9f8f5; height: 100vh; display: flex; @@ -2115,10 +2180,10 @@ onMounted(() => { .product-card-content { display: grid; - grid-template-columns: minmax(360px, 420px) minmax(760px, 1fr); + grid-template-columns: minmax(360px, 420px) minmax(360px, 440px) minmax(320px, 420px); gap: 14px; align-items: stretch; - justify-content: start; + justify-content: center; height: 100%; } @@ -2126,6 +2191,7 @@ onMounted(() => { grid-column: 2; grid-row: 1; height: var(--pc-media-h); + min-width: 0; display: flex; flex-direction: column; align-items: stretch; @@ -2133,7 +2199,7 @@ onMounted(() => { } .product-card-carousel { - width: var(--pc-media-w); + width: 100%; height: 100%; max-width: 100%; background: linear-gradient(180deg, #f6f1e6 0%, #efe6d3 100%); @@ -2146,7 +2212,7 @@ onMounted(() => { } .dialog-image-stage { - width: var(--pc-media-w); + width: 100%; max-width: 100%; height: 100%; overflow: hidden; @@ -2158,7 +2224,7 @@ onMounted(() => { } .dialog-image-empty { - width: var(--pc-media-w); + width: 100%; max-width: 100%; height: var(--pc-media-h); border: 1px dashed #c5b28d; @@ -2180,6 +2246,14 @@ onMounted(() => { overflow: auto; } +.product-card-price-panel { + grid-column: 3; + grid-row: 1; + min-width: 0; + height: var(--pc-media-h); + overflow: hidden; +} + .field-row { display: grid; grid-template-columns: 150px 1fr; @@ -2220,6 +2294,13 @@ onMounted(() => { padding: 10px; } +.product-card-price-section { + height: 100%; + margin-top: 0; + overflow: auto; + background: linear-gradient(180deg, #fffdf8 0%, #fff7ec 100%); +} + .product-card-section-title { font-size: 13px; font-weight: 800; @@ -2230,20 +2311,36 @@ onMounted(() => { .price-info-grid { display: grid; grid-template-columns: 1fr; - gap: 4px; + gap: 6px; +} + +.price-info-header { + display: grid; + grid-template-columns: 74px 1fr 1fr; + gap: 8px; + margin-bottom: 6px; + padding: 0 8px; + color: #6b5a33; + font-size: 11px; + font-weight: 800; + text-align: right; +} + +.price-info-header span:first-child { + text-align: left; } .price-info-row { display: grid; - grid-template-columns: 70px 1fr 1fr; - gap: 6px; + grid-template-columns: 74px 1fr 1fr; + gap: 8px; align-items: center; - min-height: 26px; - padding: 4px 6px; + min-height: 34px; + padding: 6px 8px; border: 1px solid #f0e5d2; border-radius: 6px; background: #fff; - font-size: 12px; + font-size: 13px; } .price-label { @@ -2253,13 +2350,49 @@ onMounted(() => { .price-value, .price-campaign { + min-height: 26px; + padding: 5px 7px; + border-radius: 5px; text-align: right; font-variant-numeric: tabular-nums; + font-weight: 800; + background: #fff; } .price-campaign { - color: #b13a2b; - font-weight: 700; + color: #8a8a8a; + background: #f4f4f4; + font-weight: 800; +} + +.price-info-row-usd .price-value { + color: #16803a; +} + +.price-info-row-eur .price-value { + color: #b91c1c; +} + +.price-info-row-try .price-value { + color: #185abc; +} + +.price-info-row-usd.has-campaign-price .price-campaign { + background: #dff6e7; + color: #0f6b2f; + font-weight: 900; +} + +.price-info-row-eur.has-campaign-price .price-campaign { + background: #fde2e2; + color: #a61717; + font-weight: 900; +} + +.price-info-row-try.has-campaign-price .price-campaign { + background: #e2edff; + color: #174ea6; + font-weight: 900; } .size-stock-grid { @@ -2342,9 +2475,12 @@ onMounted(() => { } .product-card-images, - .product-card-fields { + .product-card-fields, + .product-card-price-panel { grid-column: 1; grid-row: auto; + height: auto; + min-height: 320px; } } diff --git a/ui/src/pages/OrderPriceListUserPriceGroupMapping.vue b/ui/src/pages/OrderPriceListUserPriceGroupMapping.vue new file mode 100644 index 0000000..3d0e1ff --- /dev/null +++ b/ui/src/pages/OrderPriceListUserPriceGroupMapping.vue @@ -0,0 +1,163 @@ + + + + + Kullanici Fiyat Grubu Eslestirme + Fiyat Listesi ekraninda kullanicinin gorebilecegi fiyat gruplarini belirler. + + + + + + + + updateRowSelection(props.row.user_id, val)" + > + + + Tumunu Sec + + + Temizle + + + + + + + + + + + + Bu module erisim yetkiniz yok. + + + + + diff --git a/ui/src/pages/UserDetail.vue b/ui/src/pages/UserDetail.vue index e05a983..4842385 100644 --- a/ui/src/pages/UserDetail.vue +++ b/ui/src/pages/UserDetail.vue @@ -277,6 +277,34 @@ /> + + Fiyat Listesi Fiyat Gruplari + + + + Tumunu Sec + + + Temizle + + + + + + @@ -320,6 +348,7 @@ const { departmentOptions, piyasaOptions, nebimUserOptions, + orderPriceListPriceGroupOptions, sendingPasswordMail, lastPasswordMailSentAt } = storeToRefs(store) @@ -373,6 +402,16 @@ function clearPiyasalar () { form.value.piyasalar = [] } +function selectAllOrderPriceGroups () { + form.value.order_price_list_price_groups = (orderPriceListPriceGroupOptions.value || []) + .map((o) => o.value) + .filter(Boolean) +} + +function clearOrderPriceGroups () { + form.value.order_price_list_price_groups = [] +} + /* ================= LIFECYCLE ================= */ watch( () => userId.value, diff --git a/ui/src/router/routes.js b/ui/src/router/routes.js index 9baf52d..88036ed 100644 --- a/ui/src/router/routes.js +++ b/ui/src/router/routes.js @@ -257,6 +257,12 @@ const routes = [ component: () => import('../pages/OrderPriceListMailMapping.vue'), meta: { permission: 'system:update' } }, + { + path: 'order-price-list-user-price-groups', + name: 'order-price-list-user-price-groups', + component: () => import('../pages/OrderPriceListUserPriceGroupMapping.vue'), + meta: { permission: 'system:update' } + }, { path: 'language/translations', name: 'translation-table', diff --git a/ui/src/stores/UserDetailStore.js b/ui/src/stores/UserDetailStore.js index b1bf430..e423aa7 100644 --- a/ui/src/stores/UserDetailStore.js +++ b/ui/src/stores/UserDetailStore.js @@ -26,14 +26,16 @@ export const useUserDetailStore = defineStore('userDetail', { roles: [], departments: null, piyasalar: [], - nebim_users: null + nebim_users: null, + order_price_list_price_groups: [] }, /* ================= LOOKUPS ================= */ roleOptions: [], departmentOptions: [], piyasaOptions: [], - nebimUserOptions: [] + nebimUserOptions: [], + orderPriceListPriceGroupOptions: [] }), actions: { @@ -52,7 +54,8 @@ export const useUserDetailStore = defineStore('userDetail', { roles: [], departments: null, piyasalar: [], - nebim_users: null + nebim_users: null, + order_price_list_price_groups: [] } this.error = null this.hasPassword = false @@ -113,6 +116,7 @@ export const useUserDetailStore = defineStore('userDetail', { departments: departmentCodes.map(code => ({ code })), piyasalar: (this.form.piyasalar || []).map(code => ({ code })), + order_price_list_price_groups: this.form.order_price_list_price_groups || [], nebim_users: nebimUsernames.map(username => { const opt = (this.nebimUserOptions || []).find(x => x.value === username) @@ -146,6 +150,7 @@ export const useUserDetailStore = defineStore('userDetail', { this.form.departments = (data.departments || []).map(x => x.code)[0] || null this.form.piyasalar = (data.piyasalar || []).map(x => x.code) this.form.nebim_users = (data.nebim_users || []).map(x => x.username)[0] || null + this.form.order_price_list_price_groups = data.order_price_list_price_groups || [] this.hasPassword = !!data.has_password } catch (e) { @@ -237,17 +242,22 @@ export const useUserDetailStore = defineStore('userDetail', { ===================================================== */ async fetchLookups () { // token otomatik - const [roles, depts, piyasalar, nebims] = await Promise.all([ + const [roles, depts, piyasalar, nebims, priceGroups] = await Promise.all([ api.get('/lookups/roles'), api.get('/lookups/departments'), api.get('/lookups/piyasalar'), - api.get('/lookups/nebim-users') + api.get('/lookups/nebim-users'), + api.get('/users/order-price-list-price-groups/lookups') ]) this.roleOptions = roles?.data || roles || [] this.departmentOptions = depts?.data || depts || [] this.piyasaOptions = piyasalar?.data || piyasalar || [] this.nebimUserOptions = nebims?.data || nebims || [] + this.orderPriceListPriceGroupOptions = (priceGroups?.data?.price_groups || []).map(x => ({ + label: x.label || x.value, + value: x.value + })) } } }) diff --git a/ui/src/stores/orderPriceListUserPriceGroupStore.js b/ui/src/stores/orderPriceListUserPriceGroupStore.js new file mode 100644 index 0000000..b074970 --- /dev/null +++ b/ui/src/stores/orderPriceListUserPriceGroupStore.js @@ -0,0 +1,42 @@ +import { defineStore } from 'pinia' +import api from 'src/services/api' + +export const useOrderPriceListUserPriceGroupStore = defineStore('orderPriceListUserPriceGroup', { + state: () => ({ + loading: false, + saving: false, + rows: [], + priceGroupOptions: [] + }), + + actions: { + async fetchLookups () { + const res = await api.get('/system/order-price-list-user-price-groups/lookups') + this.priceGroupOptions = (res?.data?.price_groups || []).map((x) => ({ + label: x.label || x.value, + value: x.value + })) + }, + + async fetchRows () { + this.loading = true + try { + const res = await api.get('/system/order-price-list-user-price-groups') + this.rows = Array.isArray(res?.data) ? res.data : [] + } finally { + this.loading = false + } + }, + + async saveUserPriceGroups (userId, priceGroups) { + this.saving = true + try { + await api.put(`/system/order-price-list-user-price-groups/${encodeURIComponent(String(userId))}`, { + price_groups: Array.isArray(priceGroups) ? priceGroups : [] + }) + } finally { + this.saving = false + } + } + } +})