Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -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'
|
||||||
|
));
|
||||||
35
svc/main.go
35
svc/main.go
@@ -375,6 +375,21 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
"system", "update",
|
"system", "update",
|
||||||
wrapV3(routes.SaveOrderPriceListFirstGroupMailMappingHandler(pgDB)),
|
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,
|
bindV3(r, pgDB,
|
||||||
"/api/language/translations", "GET",
|
"/api/language/translations", "GET",
|
||||||
"language", "update",
|
"language", "update",
|
||||||
@@ -439,6 +454,21 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
"system", "update",
|
"system", "update",
|
||||||
wrapV3(routes.SaveUserPermissionsHandler(pgDB)),
|
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)
|
// ✅ permissions/routes (system:view)
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
@@ -810,6 +840,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
"order", "view",
|
"order", "view",
|
||||||
wrapV3(http.HandlerFunc(routes.GetProductPricingFilterOptionsHandler)),
|
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,
|
bindV3(r, pgDB,
|
||||||
"/api/order/price-list/campaigns", "GET",
|
"/api/order/price-list/campaigns", "GET",
|
||||||
"order", "view",
|
"order", "view",
|
||||||
|
|||||||
@@ -18,10 +18,11 @@ type UserDetail struct {
|
|||||||
HasPassword bool `json:"has_password"` // 🔐 SADECE DURUM
|
HasPassword bool `json:"has_password"` // 🔐 SADECE DURUM
|
||||||
|
|
||||||
// ===== İLİŞKİLER =====
|
// ===== İLİŞKİLER =====
|
||||||
Roles []string `json:"roles"`
|
Roles []string `json:"roles"`
|
||||||
Departments []DeptOption `json:"departments"`
|
Departments []DeptOption `json:"departments"`
|
||||||
Piyasalar []DeptOption `json:"piyasalar"`
|
Piyasalar []DeptOption `json:"piyasalar"`
|
||||||
NebimUsers []NebimOption `json:"nebim_users"`
|
NebimUsers []NebimOption `json:"nebim_users"`
|
||||||
|
OrderPriceListPriceGroups []string `json:"order_price_list_price_groups"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================================================
|
// ======================================================
|
||||||
@@ -35,10 +36,11 @@ type UserWrite struct {
|
|||||||
Mobile string `json:"mobile"`
|
Mobile string `json:"mobile"`
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
|
|
||||||
Roles []string `json:"roles"`
|
Roles []string `json:"roles"`
|
||||||
Departments []DeptOption `json:"departments"`
|
Departments []DeptOption `json:"departments"`
|
||||||
Piyasalar []DeptOption `json:"piyasalar"`
|
Piyasalar []DeptOption `json:"piyasalar"`
|
||||||
NebimUsers []NebimOption `json:"nebim_users"`
|
NebimUsers []NebimOption `json:"nebim_users"`
|
||||||
|
OrderPriceListPriceGroups []string `json:"order_price_list_price_groups"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================================================
|
// ======================================================
|
||||||
|
|||||||
@@ -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 err := tx.Commit(); err != nil {
|
||||||
if pe, ok := err.(*pq.Error); ok {
|
if pe, ok := err.(*pq.Error); ok {
|
||||||
log.Printf(
|
log.Printf(
|
||||||
|
|||||||
278
svc/routes/order_price_list_user_price_groups.go
Normal file
278
svc/routes/order_price_list_user_price_groups.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
// 🟢 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 {
|
if err := tx.Commit(); err != nil {
|
||||||
log.Printf("❌ [UserDetail] commit failed user_id=%d err=%v", userID, err)
|
log.Printf("❌ [UserDetail] commit failed user_id=%d err=%v", userID, err)
|
||||||
http.Error(w, "Commit başarısız", http.StatusInternalServerError)
|
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_dprt WHERE dfusr_id = $1`,
|
||||||
`DELETE FROM dfusr_piyasa WHERE dfusr_id = $1`,
|
`DELETE FROM dfusr_piyasa WHERE dfusr_id = $1`,
|
||||||
`DELETE FROM dfusr_nebim_user 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 {
|
isUndefinedTable := func(err error) bool {
|
||||||
|
|||||||
@@ -418,6 +418,11 @@ const menuItems = [
|
|||||||
label: 'Fiyat Listesi Mail Eşleştirme',
|
label: 'Fiyat Listesi Mail Eşleştirme',
|
||||||
to: '/app/order-price-list-mail-mapping',
|
to: '/app/order-price-list-mail-mapping',
|
||||||
permission: 'system:update'
|
permission: 'system:update'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Kullanıcı Fiyat Eşleştirme',
|
||||||
|
to: '/app/order-price-list-user-price-groups',
|
||||||
|
permission: 'system:update'
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
<q-item-section>Tumunu Temizle</q-item-section>
|
<q-item-section>Tumunu Temizle</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-separator />
|
<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-item-section avatar>
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
dense
|
dense
|
||||||
@@ -165,7 +165,7 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="top-x-scroll-inner"
|
class="top-x-scroll-inner"
|
||||||
:style="{ width: `${tableMinWidth}px` }"
|
:style="{ width: `${tableScrollWidth}px` }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<q-table
|
<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">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="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">
|
||||||
<div class="product-card-section-title">Beden Stoklari</div>
|
<div class="product-card-section-title">Beden Stoklari</div>
|
||||||
<q-inner-loading :showing="productCardStockLoading">
|
<q-inner-loading :showing="productCardStockLoading">
|
||||||
@@ -615,6 +603,25 @@
|
|||||||
<div v-else-if="!productCardStockLoading" class="product-card-empty-text">Beden stogu bulunamadi.</div>
|
<div v-else-if="!productCardStockLoading" class="product-card-empty-text">Beden stogu bulunamadi.</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -664,10 +671,18 @@ import api from 'src/services/api'
|
|||||||
const PAGE_LIMIT = 250
|
const PAGE_LIMIT = 250
|
||||||
const GUIDANCE_MSG = 'Liste icin filtre secin.'
|
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}`,
|
label: `${cur} ${lv}`,
|
||||||
value: `${cur.toLowerCase()}${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 campaignPairs = priceOptions.map((x) => ({ base: x.value, derived: `${x.value}Campaign` }))
|
||||||
const priceColumnNames = campaignPairs.flatMap((p) => [p.base, p.derived])
|
const priceColumnNames = campaignPairs.flatMap((p) => [p.base, p.derived])
|
||||||
|
|
||||||
@@ -721,8 +736,10 @@ const productCardPriceRows = computed(() => {
|
|||||||
.map((option) => ({
|
.map((option) => ({
|
||||||
key: option.value,
|
key: option.value,
|
||||||
label: option.label,
|
label: option.label,
|
||||||
|
currency: String(option.value || '').slice(0, 3).toLowerCase(),
|
||||||
price: formatPrice(row?.[option.value]),
|
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 || []))
|
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) {
|
function onTopFilterSearchUrunIlkGrubu (val, update) {
|
||||||
update(() => {
|
update(() => {
|
||||||
filterSearch.value.urunIlkGrubu = toText(val)
|
filterSearch.value.urunIlkGrubu = toText(val)
|
||||||
@@ -1306,20 +1337,33 @@ function onPageChange (page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function togglePriceOption (value) {
|
function togglePriceOption (value) {
|
||||||
|
if (!allowedPriceOptions.value.some((x) => x.value === value)) return
|
||||||
const set = new Set(selectedPriceOptions.value || [])
|
const set = new Set(selectedPriceOptions.value || [])
|
||||||
if (set.has(value)) set.delete(value)
|
if (set.has(value)) set.delete(value)
|
||||||
else set.add(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 () {
|
function selectAllPrices () {
|
||||||
selectedPriceOptions.value = priceOptions.map((x) => x.value)
|
selectedPriceOptions.value = allowedPriceOptions.value.map((x) => x.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAllPrices () {
|
function clearAllPrices () {
|
||||||
selectedPriceOptions.value = []
|
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 = {}) {
|
function col (name, label, field, width, extra = {}) {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
@@ -1402,6 +1446,7 @@ const filteredRows = computed(() => {
|
|||||||
return list
|
return list
|
||||||
})
|
})
|
||||||
const tableMinWidth = computed(() => visibleColumns.value.reduce((sum, c) => sum + extractWidth(c.style), 0))
|
const tableMinWidth = computed(() => visibleColumns.value.reduce((sum, c) => sum + extractWidth(c.style), 0))
|
||||||
|
const tableScrollWidth = computed(() => tableMinWidth.value + stickyScrollComp.value + 48)
|
||||||
const tableStyle = computed(() => ({
|
const tableStyle = computed(() => ({
|
||||||
width: `${tableMinWidth.value}px`,
|
width: `${tableMinWidth.value}px`,
|
||||||
minWidth: `${tableMinWidth.value}px`,
|
minWidth: `${tableMinWidth.value}px`,
|
||||||
@@ -1609,7 +1654,12 @@ watch([tableMinWidth, rows], async () => {
|
|||||||
bindTableScrollSync()
|
bindTableScrollSync()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(allowedPriceOptions, () => {
|
||||||
|
normalizeSelectedPriceOptions()
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
void fetchMyPriceGroups()
|
||||||
void fetchServerFilterOptions('urunIlkGrubu', '')
|
void fetchServerFilterOptions('urunIlkGrubu', '')
|
||||||
void fetchServerFilterOptions('urunAnaGrubu', '')
|
void fetchServerFilterOptions('urunAnaGrubu', '')
|
||||||
void fetchServerFilterOptions('productCode', '')
|
void fetchServerFilterOptions('productCode', '')
|
||||||
@@ -1885,23 +1935,23 @@ onMounted(() => {
|
|||||||
|
|
||||||
.pricing-table :deep(th.usd-col),
|
.pricing-table :deep(th.usd-col),
|
||||||
.pricing-table :deep(td.usd-col) {
|
.pricing-table :deep(td.usd-col) {
|
||||||
background: #ecf9f0;
|
background: #fff;
|
||||||
color: #178a3e;
|
color: #16803a;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pricing-table :deep(th.eur-col),
|
.pricing-table :deep(th.eur-col),
|
||||||
.pricing-table :deep(td.eur-col) {
|
.pricing-table :deep(td.eur-col) {
|
||||||
background: #fdeeee;
|
background: #fff;
|
||||||
color: #c62828;
|
color: #b91c1c;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pricing-table :deep(th.try-col),
|
.pricing-table :deep(th.try-col),
|
||||||
.pricing-table :deep(td.try-col) {
|
.pricing-table :deep(td.try-col) {
|
||||||
background: #edf4ff;
|
background: #fff;
|
||||||
color: #1e63c6;
|
color: #185abc;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pricing-table :deep(th.usd-col),
|
.pricing-table :deep(th.usd-col),
|
||||||
@@ -1910,14 +1960,30 @@ onMounted(() => {
|
|||||||
.pricing-table :deep(td.usd-col),
|
.pricing-table :deep(td.usd-col),
|
||||||
.pricing-table :deep(td.eur-col),
|
.pricing-table :deep(td.eur-col),
|
||||||
.pricing-table :deep(td.try-col) {
|
.pricing-table :deep(td.try-col) {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pricing-table :deep(td.campaign-price-col),
|
.pricing-table :deep(th.usd-col.campaign-price-col),
|
||||||
.pricing-table :deep(th.campaign-price-col) {
|
.pricing-table :deep(td.usd-col.campaign-price-col) {
|
||||||
background: #fff3f1;
|
background: #dff6e7;
|
||||||
color: #c62828;
|
color: #0f6b2f;
|
||||||
font-weight: 800;
|
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;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1989,7 +2055,6 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.campaign-price-text {
|
.campaign-price-text {
|
||||||
color: #c62828;
|
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2100,7 +2165,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
.product-card-dialog {
|
.product-card-dialog {
|
||||||
--pc-media-h: calc(100vh - 180px);
|
--pc-media-h: calc(100vh - 180px);
|
||||||
--pc-media-w: min(74vw, 1220px);
|
--pc-media-w: min(28vw, 440px);
|
||||||
background: #f9f8f5;
|
background: #f9f8f5;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2115,10 +2180,10 @@ onMounted(() => {
|
|||||||
|
|
||||||
.product-card-content {
|
.product-card-content {
|
||||||
display: grid;
|
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;
|
gap: 14px;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-content: start;
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2126,6 +2191,7 @@ onMounted(() => {
|
|||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
height: var(--pc-media-h);
|
height: var(--pc-media-h);
|
||||||
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
@@ -2133,7 +2199,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.product-card-carousel {
|
.product-card-carousel {
|
||||||
width: var(--pc-media-w);
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
background: linear-gradient(180deg, #f6f1e6 0%, #efe6d3 100%);
|
background: linear-gradient(180deg, #f6f1e6 0%, #efe6d3 100%);
|
||||||
@@ -2146,7 +2212,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dialog-image-stage {
|
.dialog-image-stage {
|
||||||
width: var(--pc-media-w);
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -2158,7 +2224,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dialog-image-empty {
|
.dialog-image-empty {
|
||||||
width: var(--pc-media-w);
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: var(--pc-media-h);
|
height: var(--pc-media-h);
|
||||||
border: 1px dashed #c5b28d;
|
border: 1px dashed #c5b28d;
|
||||||
@@ -2180,6 +2246,14 @@ onMounted(() => {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-card-price-panel {
|
||||||
|
grid-column: 3;
|
||||||
|
grid-row: 1;
|
||||||
|
min-width: 0;
|
||||||
|
height: var(--pc-media-h);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.field-row {
|
.field-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 150px 1fr;
|
grid-template-columns: 150px 1fr;
|
||||||
@@ -2220,6 +2294,13 @@ onMounted(() => {
|
|||||||
padding: 10px;
|
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 {
|
.product-card-section-title {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
@@ -2230,20 +2311,36 @@ onMounted(() => {
|
|||||||
.price-info-grid {
|
.price-info-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
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 {
|
.price-info-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 70px 1fr 1fr;
|
grid-template-columns: 74px 1fr 1fr;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 26px;
|
min-height: 34px;
|
||||||
padding: 4px 6px;
|
padding: 6px 8px;
|
||||||
border: 1px solid #f0e5d2;
|
border: 1px solid #f0e5d2;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-label {
|
.price-label {
|
||||||
@@ -2253,13 +2350,49 @@ onMounted(() => {
|
|||||||
|
|
||||||
.price-value,
|
.price-value,
|
||||||
.price-campaign {
|
.price-campaign {
|
||||||
|
min-height: 26px;
|
||||||
|
padding: 5px 7px;
|
||||||
|
border-radius: 5px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-weight: 800;
|
||||||
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-campaign {
|
.price-campaign {
|
||||||
color: #b13a2b;
|
color: #8a8a8a;
|
||||||
font-weight: 700;
|
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 {
|
.size-stock-grid {
|
||||||
@@ -2342,9 +2475,12 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.product-card-images,
|
.product-card-images,
|
||||||
.product-card-fields {
|
.product-card-fields,
|
||||||
|
.product-card-price-panel {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: auto;
|
grid-row: auto;
|
||||||
|
height: auto;
|
||||||
|
min-height: 320px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
163
ui/src/pages/OrderPriceListUserPriceGroupMapping.vue
Normal file
163
ui/src/pages/OrderPriceListUserPriceGroupMapping.vue
Normal 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>
|
||||||
@@ -277,6 +277,34 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -320,6 +348,7 @@ const {
|
|||||||
departmentOptions,
|
departmentOptions,
|
||||||
piyasaOptions,
|
piyasaOptions,
|
||||||
nebimUserOptions,
|
nebimUserOptions,
|
||||||
|
orderPriceListPriceGroupOptions,
|
||||||
sendingPasswordMail,
|
sendingPasswordMail,
|
||||||
lastPasswordMailSentAt
|
lastPasswordMailSentAt
|
||||||
} = storeToRefs(store)
|
} = storeToRefs(store)
|
||||||
@@ -373,6 +402,16 @@ function clearPiyasalar () {
|
|||||||
form.value.piyasalar = []
|
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 ================= */
|
/* ================= LIFECYCLE ================= */
|
||||||
watch(
|
watch(
|
||||||
() => userId.value,
|
() => userId.value,
|
||||||
|
|||||||
@@ -257,6 +257,12 @@ const routes = [
|
|||||||
component: () => import('../pages/OrderPriceListMailMapping.vue'),
|
component: () => import('../pages/OrderPriceListMailMapping.vue'),
|
||||||
meta: { permission: 'system:update' }
|
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',
|
path: 'language/translations',
|
||||||
name: 'translation-table',
|
name: 'translation-table',
|
||||||
|
|||||||
@@ -26,14 +26,16 @@ export const useUserDetailStore = defineStore('userDetail', {
|
|||||||
roles: [],
|
roles: [],
|
||||||
departments: null,
|
departments: null,
|
||||||
piyasalar: [],
|
piyasalar: [],
|
||||||
nebim_users: null
|
nebim_users: null,
|
||||||
|
order_price_list_price_groups: []
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ================= LOOKUPS ================= */
|
/* ================= LOOKUPS ================= */
|
||||||
roleOptions: [],
|
roleOptions: [],
|
||||||
departmentOptions: [],
|
departmentOptions: [],
|
||||||
piyasaOptions: [],
|
piyasaOptions: [],
|
||||||
nebimUserOptions: []
|
nebimUserOptions: [],
|
||||||
|
orderPriceListPriceGroupOptions: []
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
@@ -52,7 +54,8 @@ export const useUserDetailStore = defineStore('userDetail', {
|
|||||||
roles: [],
|
roles: [],
|
||||||
departments: null,
|
departments: null,
|
||||||
piyasalar: [],
|
piyasalar: [],
|
||||||
nebim_users: null
|
nebim_users: null,
|
||||||
|
order_price_list_price_groups: []
|
||||||
}
|
}
|
||||||
this.error = null
|
this.error = null
|
||||||
this.hasPassword = false
|
this.hasPassword = false
|
||||||
@@ -113,6 +116,7 @@ export const useUserDetailStore = defineStore('userDetail', {
|
|||||||
departments: departmentCodes.map(code => ({ code })),
|
departments: departmentCodes.map(code => ({ code })),
|
||||||
|
|
||||||
piyasalar: (this.form.piyasalar || []).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 => {
|
nebim_users: nebimUsernames.map(username => {
|
||||||
const opt = (this.nebimUserOptions || []).find(x => x.value === 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.departments = (data.departments || []).map(x => x.code)[0] || null
|
||||||
this.form.piyasalar = (data.piyasalar || []).map(x => x.code)
|
this.form.piyasalar = (data.piyasalar || []).map(x => x.code)
|
||||||
this.form.nebim_users = (data.nebim_users || []).map(x => x.username)[0] || null
|
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
|
this.hasPassword = !!data.has_password
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -237,17 +242,22 @@ export const useUserDetailStore = defineStore('userDetail', {
|
|||||||
===================================================== */
|
===================================================== */
|
||||||
async fetchLookups () {
|
async fetchLookups () {
|
||||||
// token otomatik
|
// 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/roles'),
|
||||||
api.get('/lookups/departments'),
|
api.get('/lookups/departments'),
|
||||||
api.get('/lookups/piyasalar'),
|
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.roleOptions = roles?.data || roles || []
|
||||||
this.departmentOptions = depts?.data || depts || []
|
this.departmentOptions = depts?.data || depts || []
|
||||||
this.piyasaOptions = piyasalar?.data || piyasalar || []
|
this.piyasaOptions = piyasalar?.data || piyasalar || []
|
||||||
this.nebimUserOptions = nebims?.data || nebims || []
|
this.nebimUserOptions = nebims?.data || nebims || []
|
||||||
|
this.orderPriceListPriceGroupOptions = (priceGroups?.data?.price_groups || []).map(x => ({
|
||||||
|
label: x.label || x.value,
|
||||||
|
value: x.value
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
42
ui/src/stores/orderPriceListUserPriceGroupStore.js
Normal file
42
ui/src/stores/orderPriceListUserPriceGroupStore.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user