Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-06-19 15:44:12 +03:00
parent da9d7c2fd5
commit 1054a15547
13 changed files with 828 additions and 62 deletions

View File

@@ -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'
));

View File

@@ -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",

View File

@@ -22,6 +22,7 @@ type UserDetail struct {
Departments []DeptOption `json:"departments"`
Piyasalar []DeptOption `json:"piyasalar"`
NebimUsers []NebimOption `json:"nebim_users"`
OrderPriceListPriceGroups []string `json:"order_price_list_price_groups"`
}
// ======================================================
@@ -39,6 +40,7 @@ type UserWrite struct {
Departments []DeptOption `json:"departments"`
Piyasalar []DeptOption `json:"piyasalar"`
NebimUsers []NebimOption `json:"nebim_users"`
OrderPriceListPriceGroups []string `json:"order_price_list_price_groups"`
}
// ======================================================

View File

@@ -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(

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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'
}
]

View File

@@ -80,7 +80,7 @@
<q-item-section>Tumunu Temizle</q-item-section>
</q-item>
<q-separator />
<q-item v-for="option in priceOptions" :key="option.value" clickable @click="togglePriceOption(option.value)">
<q-item v-for="option in allowedPriceOptions" :key="option.value" clickable @click="togglePriceOption(option.value)">
<q-item-section avatar>
<q-checkbox
dense
@@ -165,7 +165,7 @@
>
<div
class="top-x-scroll-inner"
:style="{ width: `${tableMinWidth}px` }"
:style="{ width: `${tableScrollWidth}px` }"
/>
</div>
<q-table
@@ -589,18 +589,6 @@
<div class="field-row"><span class="k">Kampanya</span><span class="v">{{ productCardData.campaignLabel || '-' }}</span></div>
<div class="field-row"><span class="k">Stok</span><span class="v">{{ formatStock(productCardData.stockQty || 0) }}</span></div>
<div class="product-card-section">
<div class="product-card-section-title">Fiyat Bilgileri</div>
<div v-if="productCardPriceRows.length" class="price-info-grid">
<div v-for="item in productCardPriceRows" :key="item.key" class="price-info-row">
<span class="price-label">{{ item.label }}</span>
<span class="price-value">{{ item.price || '-' }}</span>
<span class="price-campaign">{{ item.campaignPrice || '-' }}</span>
</div>
</div>
<div v-else class="product-card-empty-text">Secili fiyat kolonu yok.</div>
</div>
<div class="product-card-section">
<div class="product-card-section-title">Beden Stoklari</div>
<q-inner-loading :showing="productCardStockLoading">
@@ -615,6 +603,25 @@
<div v-else-if="!productCardStockLoading" class="product-card-empty-text">Beden stogu bulunamadi.</div>
</div>
</div>
<div class="product-card-price-panel">
<div class="product-card-section product-card-price-section">
<div class="product-card-section-title">Fiyat Bilgileri</div>
<div class="price-info-header">
<span>Fiyat</span>
<span>Liste</span>
<span>Kampanyali</span>
</div>
<div v-if="productCardPriceRows.length" class="price-info-grid">
<div v-for="item in productCardPriceRows" :key="item.key" :class="['price-info-row', `price-info-row-${item.currency}`, { 'has-campaign-price': item.hasCampaignPrice }]">
<span class="price-label">{{ item.label }}</span>
<span class="price-value">{{ item.price || '-' }}</span>
<span class="price-campaign">{{ item.campaignPrice || '-' }}</span>
</div>
</div>
<div v-else class="product-card-empty-text">Secili fiyat kolonu yok.</div>
</div>
</div>
</div>
</q-card-section>
</q-card>
@@ -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;
}
}
</style>

View File

@@ -0,0 +1,163 @@
<template>
<q-page v-if="canUpdateSystem" class="q-pa-md">
<div class="row items-center justify-between q-mb-md">
<div>
<div class="text-h6">Kullanici Fiyat Grubu Eslestirme</div>
<div class="text-caption text-grey-7">Fiyat Listesi ekraninda kullanicinin gorebilecegi fiyat gruplarini belirler.</div>
</div>
<q-btn
color="primary"
icon="save"
label="Degisiklikleri Kaydet"
:loading="store.saving"
:disable="!hasChanges"
@click="saveChanges"
/>
</div>
<q-table
flat
bordered
dense
row-key="user_id"
:loading="store.loading"
:rows="store.rows"
:columns="columns"
:rows-per-page-options="[0]"
:pagination="{ rowsPerPage: 0 }"
>
<template #body-cell-price_groups="props">
<q-td :props="props">
<q-select
:model-value="editableByUser[props.row.user_id] || []"
:options="store.priceGroupOptions"
option-value="value"
option-label="label"
emit-value
map-options
multiple
use-chips
clearable
dense
outlined
label="Fiyat gruplari"
@update:model-value="(val) => updateRowSelection(props.row.user_id, val)"
>
<template #before-options>
<q-item clickable @click="selectAll(props.row.user_id)">
<q-item-section>Tumunu Sec</q-item-section>
</q-item>
<q-item clickable @click="clearAll(props.row.user_id)">
<q-item-section>Temizle</q-item-section>
</q-item>
<q-separator />
</template>
</q-select>
</q-td>
</template>
</q-table>
</q-page>
<q-page v-else class="q-pa-md flex flex-center">
<div class="text-negative text-subtitle1">
Bu module erisim yetkiniz yok.
</div>
</q-page>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { useQuasar } from 'quasar'
import { usePermission } from 'src/composables/usePermission'
import { useOrderPriceListUserPriceGroupStore } from 'src/stores/orderPriceListUserPriceGroupStore'
const $q = useQuasar()
const store = useOrderPriceListUserPriceGroupStore()
const { canUpdate } = usePermission()
const canUpdateSystem = canUpdate('system')
const editableByUser = ref({})
const originalByUser = ref({})
const columns = [
{ name: 'username', label: 'Kullanici Kodu', field: 'username', align: 'left', sortable: true },
{ name: 'full_name', label: 'Ad Soyad', field: 'full_name', align: 'left', sortable: true },
{ name: 'email', label: 'E-Posta', field: 'email', align: 'left' },
{ name: 'price_groups', label: 'Gorebilecegi Fiyat Gruplari', field: 'price_groups', align: 'left' }
]
const changedUsers = computed(() => {
return (store.rows || [])
.map((r) => Number(r.user_id || 0))
.filter(Boolean)
.filter((id) => !isEqualList(normalizeList(editableByUser.value[id] || []), normalizeList(originalByUser.value[id] || [])))
})
const hasChanges = computed(() => changedUsers.value.length > 0)
function normalizeList (list) {
const allowed = new Set((store.priceGroupOptions || []).map((x) => x.value))
return Array.from(new Set((Array.isArray(list) ? list : []).map((x) => String(x).trim()).filter((x) => allowed.has(x)))).sort()
}
function isEqualList (a, b) {
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i += 1) {
if (a[i] !== b[i]) return false
}
return true
}
function initEditableState () {
const editable = {}
const original = {}
for (const row of store.rows || []) {
const id = Number(row.user_id || 0)
if (!id) continue
const selected = normalizeList(row.price_groups || [])
editable[id] = [...selected]
original[id] = [...selected]
}
editableByUser.value = editable
originalByUser.value = original
}
function updateRowSelection (userId, newValue) {
const id = Number(userId || 0)
if (!id) return
editableByUser.value = { ...editableByUser.value, [id]: normalizeList(newValue) }
}
function selectAll (userId) {
updateRowSelection(userId, store.priceGroupOptions.map((x) => x.value))
}
function clearAll (userId) {
updateRowSelection(userId, [])
}
async function init () {
try {
await Promise.all([store.fetchLookups(), store.fetchRows()])
initEditableState()
} catch (err) {
$q.notify({ type: 'negative', message: err?.message || 'Kullanici fiyat grubu eslestirmeleri yuklenemedi' })
}
}
async function saveChanges () {
if (!hasChanges.value) return
try {
for (const id of changedUsers.value) {
await store.saveUserPriceGroups(id, editableByUser.value[id] || [])
}
await store.fetchRows()
initEditableState()
$q.notify({ type: 'positive', message: 'Degisiklikler kaydedildi' })
} catch (err) {
$q.notify({ type: 'negative', message: err?.message || 'Kayit hatasi' })
}
}
onMounted(() => { init() })
</script>

View File

@@ -277,6 +277,34 @@
/>
</div>
<div class="col-12">
<div class="text-caption text-grey-7 q-mb-xs">Fiyat Listesi Fiyat Gruplari</div>
<q-select
v-model="form.order_price_list_price_groups"
:options="orderPriceListPriceGroupOptions"
option-label="label"
option-value="value"
emit-value
map-options
multiple
use-chips
clearable
dense
filled
behavior="menu"
>
<template #before-options>
<q-item clickable @click="selectAllOrderPriceGroups">
<q-item-section>Tumunu Sec</q-item-section>
</q-item>
<q-item clickable @click="clearOrderPriceGroups">
<q-item-section>Temizle</q-item-section>
</q-item>
<q-separator />
</template>
</q-select>
</div>
</div>
</q-card-section>
</q-card>
@@ -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,

View File

@@ -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',

View File

@@ -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
}))
}
}
})

View File

@@ -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
}
}
}
})