From 7512e7fe7c5a04c8d26d99366319901509871d8d Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Fri, 19 Jun 2026 00:22:44 +0300 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- svc/main.go | 20 ++ svc/queries/first_group_mail_mapping.go | 23 ++ svc/routes/first_group_mail_mapping.go | 208 ++++++++++++++++++ svc/routes/order_price_list_export_notify.go | 192 ++++++++++++++++ ui/src/layouts/MainLayout.vue | 5 + ui/src/pages/OrderPriceList.vue | 74 ++++++- ui/src/pages/OrderPriceListMailMapping.vue | 172 +++++++++++++++ ui/src/router/routes.js | 6 + .../stores/orderPriceListMailMappingStore.js | 47 ++++ 9 files changed, 735 insertions(+), 12 deletions(-) create mode 100644 svc/routes/order_price_list_export_notify.go create mode 100644 ui/src/pages/OrderPriceListMailMapping.vue create mode 100644 ui/src/stores/orderPriceListMailMappingStore.js diff --git a/svc/main.go b/svc/main.go index ce3c7fa..b1886c6 100644 --- a/svc/main.go +++ b/svc/main.go @@ -360,6 +360,21 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "system", "update", wrapV3(routes.SavePricingFirstGroupMailMappingHandler(pgDB)), ) + bindV3(r, pgDB, + "/api/system/order-price-list-mail-mappings/lookups", "GET", + "system", "update", + wrapV3(routes.GetOrderPriceListFirstGroupMailMappingLookupsHandler(pgDB)), + ) + bindV3(r, pgDB, + "/api/system/order-price-list-mail-mappings", "GET", + "system", "update", + wrapV3(routes.GetOrderPriceListFirstGroupMailMappingsHandler(pgDB)), + ) + bindV3(r, pgDB, + "/api/system/order-price-list-mail-mappings/{group}", "PUT", + "system", "update", + wrapV3(routes.SaveOrderPriceListFirstGroupMailMappingHandler(pgDB)), + ) bindV3(r, pgDB, "/api/language/translations", "GET", "language", "update", @@ -815,6 +830,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "order", "view", wrapV3(http.HandlerFunc(routes.ExportProductPriceListPDFHandler(pgDB))), ) + bindV3(r, pgDB, + "/api/order/price-list/export-notify", "POST", + "order", "view", + wrapV3(http.HandlerFunc(routes.NotifyOrderPriceListExportHandler(pgDB, ml))), + ) bindV3(r, pgDB, "/api/product-size-match/rules", "GET", "order", "view", diff --git a/svc/queries/first_group_mail_mapping.go b/svc/queries/first_group_mail_mapping.go index a6dd9b2..09d1977 100644 --- a/svc/queries/first_group_mail_mapping.go +++ b/svc/queries/first_group_mail_mapping.go @@ -45,3 +45,26 @@ const InsertPricingFirstGroupMailMapping = ` INSERT INTO mk_pricing_first_group_mail (urun_ilk_grubu, mail_id) VALUES ($1, $2) ` + +const GetOrderPriceListFirstGroupMailMappingRows = ` +SELECT + f.urun_ilk_grubu, + m.id::text, + m.email, + COALESCE(NULLIF(m.display_name, ''), m.email) AS display_name +FROM mk_order_price_list_first_group_mail f +JOIN mk_mail m + ON m.id = f.mail_id + AND m.is_active = true +ORDER BY f.urun_ilk_grubu, m.email +` + +const DeleteOrderPriceListFirstGroupMailsByGroup = ` +DELETE FROM mk_order_price_list_first_group_mail +WHERE urun_ilk_grubu = $1 +` + +const InsertOrderPriceListFirstGroupMailMapping = ` +INSERT INTO mk_order_price_list_first_group_mail (urun_ilk_grubu, mail_id) +VALUES ($1, $2) +` diff --git a/svc/routes/first_group_mail_mapping.go b/svc/routes/first_group_mail_mapping.go index e487ef7..c5fa724 100644 --- a/svc/routes/first_group_mail_mapping.go +++ b/svc/routes/first_group_mail_mapping.go @@ -48,6 +48,17 @@ CREATE TABLE IF NOT EXISTS mk_pricing_first_group_mail ( ) `, `CREATE INDEX IF NOT EXISTS ix_pricing_first_group_mail_group ON mk_pricing_first_group_mail (urun_ilk_grubu)`, + ` +CREATE TABLE IF NOT EXISTS mk_order_price_list_first_group_mail ( + urun_ilk_grubu TEXT NOT NULL, + mail_id UUID NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (urun_ilk_grubu, mail_id), + CONSTRAINT fk_order_price_list_first_group_mail_mail + FOREIGN KEY (mail_id) REFERENCES mk_mail(id) ON DELETE CASCADE +) +`, + `CREATE INDEX IF NOT EXISTS ix_order_price_list_first_group_mail_group ON mk_order_price_list_first_group_mail (urun_ilk_grubu)`, } for _, s := range stmts { @@ -130,6 +141,11 @@ func GetPricingFirstGroupMailMappingLookupsHandler(pg *sql.DB) http.HandlerFunc return GetCostingFirstGroupMailMappingLookupsHandler(pg) } +func GetOrderPriceListFirstGroupMailMappingLookupsHandler(pg *sql.DB) http.HandlerFunc { + // same lookups as costing/pricing + return GetCostingFirstGroupMailMappingLookupsHandler(pg) +} + func GetCostingFirstGroupMailMappingsHandler(pg *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") @@ -492,3 +508,195 @@ func SavePricingFirstGroupMailMappingHandler(pg *sql.DB) http.HandlerFunc { }) } } + +func GetOrderPriceListFirstGroupMailMappingsHandler(pg *sql.DB) http.HandlerFunc { + return getFirstGroupMailMappingsByQuery(pg, queries.GetOrderPriceListFirstGroupMailMappingRows) +} + +func SaveOrderPriceListFirstGroupMailMappingHandler(pg *sql.DB) http.HandlerFunc { + return saveFirstGroupMailMappingByQueries( + pg, + queries.DeleteOrderPriceListFirstGroupMailsByGroup, + queries.InsertOrderPriceListFirstGroupMailMapping, + ) +} + +func getFirstGroupMailMappingsByQuery(pg *sql.DB, mappingQuery string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + mssql := db.GetDB() + if mssql == nil { + http.Error(w, "mssql connection not available", http.StatusServiceUnavailable) + return + } + if err := ensureFirstGroupMailMappingTables(pg); err != nil { + http.Error(w, "mapping table bootstrap error", http.StatusInternalServerError) + return + } + + traceID := utils.TraceIDFromRequest(r) + ctx := utils.ContextWithTraceID(r.Context(), traceID) + + allCodes := make([]string, 0, 512) + titleByCode := make(map[string]string, 512) + fgRows, err := queries.ListProductFirstGroupCodeDescOptions(ctx, mssql, "", 5000) + if err != nil { + http.Error(w, "first group lookup error", http.StatusInternalServerError) + return + } + defer fgRows.Close() + for fgRows.Next() { + var code string + var title string + if err := fgRows.Scan(&code, &title); err != nil { + http.Error(w, "first group scan error", http.StatusInternalServerError) + return + } + code = strings.TrimSpace(code) + title = strings.TrimSpace(title) + if code != "" { + allCodes = append(allCodes, code) + if _, ok := titleByCode[code]; !ok { + titleByCode[code] = title + } + } + } + if err := fgRows.Err(); err != nil { + http.Error(w, "first group rows error", http.StatusInternalServerError) + return + } + allCodes = normalizeIDList(allCodes) + + rows, err := pg.Query(mappingQuery) + if err != nil { + http.Error(w, "mapping query error", http.StatusInternalServerError) + return + } + defer rows.Close() + + byGroup := map[string]*models.FirstGroupMailMappingRow{} + for _, code := range allCodes { + byGroup[code] = &models.FirstGroupMailMappingRow{ + UrunIlkGrubu: code, + GroupCode: code, + GroupTitle: titleByCode[code], + MailIDs: make([]string, 0, 8), + Mails: make([]models.FirstGroupMailOption, 0, 8), + } + } + + for rows.Next() { + var group sql.NullString + var mailID sql.NullString + var email sql.NullString + var displayName sql.NullString + if err := rows.Scan(&group, &mailID, &email, &displayName); err != nil { + http.Error(w, "mapping scan error", http.StatusInternalServerError) + return + } + code := strings.TrimSpace(group.String) + if code == "" { + continue + } + row, ok := byGroup[code] + if !ok { + row = &models.FirstGroupMailMappingRow{ + UrunIlkGrubu: code, + GroupCode: code, + GroupTitle: titleByCode[code], + MailIDs: make([]string, 0, 8), + Mails: make([]models.FirstGroupMailOption, 0, 8), + } + byGroup[code] = row + allCodes = append(allCodes, code) + } + if mailID.Valid && strings.TrimSpace(mailID.String) != "" { + id := strings.TrimSpace(mailID.String) + row.MailIDs = append(row.MailIDs, id) + label := strings.TrimSpace(displayName.String) + if label == "" { + label = strings.TrimSpace(email.String) + } + row.Mails = append(row.Mails, models.FirstGroupMailOption{ID: id, Label: label}) + } + } + if err := rows.Err(); err != nil { + http.Error(w, "mapping rows error", http.StatusInternalServerError) + return + } + + allCodes = normalizeIDList(allCodes) + out := make([]models.FirstGroupMailMappingRow, 0, len(allCodes)) + for _, code := range allCodes { + if r := byGroup[code]; r != nil { + r.MailIDs = normalizeIDList(r.MailIDs) + if strings.TrimSpace(r.GroupTitle) == "" { + r.GroupTitle = titleByCode[code] + } + out = append(out, *r) + } + } + _ = json.NewEncoder(w).Encode(out) + } +} + +func saveFirstGroupMailMappingByQueries(pg *sql.DB, deleteQuery string, insertQuery string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + group := strings.TrimSpace(mux.Vars(r)["group"]) + if group == "" { + http.Error(w, "invalid urun_ilk_grubu", http.StatusBadRequest) + return + } + + var payload FirstGroupMailSavePayload + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, "invalid payload", http.StatusBadRequest) + return + } + + mailIDs := normalizeIDList(payload.MailIDs) + for _, mailID := range mailIDs { + var mailExists bool + if err := pg.QueryRow(queries.ExistsActiveMailByID, mailID).Scan(&mailExists); err != nil { + http.Error(w, "mail validate error", http.StatusInternalServerError) + return + } + if !mailExists { + http.Error(w, "mail not found: "+mailID, http.StatusBadRequest) + return + } + } + + tx, err := pg.Begin() + if err != nil { + http.Error(w, "transaction start error", http.StatusInternalServerError) + return + } + defer tx.Rollback() + + if _, err := tx.Exec(deleteQuery, group); err != nil { + http.Error(w, "mapping delete error", http.StatusInternalServerError) + return + } + for _, mailID := range mailIDs { + if _, err := tx.Exec(insertQuery, group, mailID); err != nil { + http.Error(w, "mapping insert error", http.StatusInternalServerError) + return + } + } + + if err := tx.Commit(); err != nil { + http.Error(w, "transaction commit error", http.StatusInternalServerError) + return + } + + _ = json.NewEncoder(w).Encode(map[string]any{ + "success": true, + "urun_ilk_grubu": group, + "mail_ids": mailIDs, + }) + } +} diff --git a/svc/routes/order_price_list_export_notify.go b/svc/routes/order_price_list_export_notify.go new file mode 100644 index 0000000..27ea308 --- /dev/null +++ b/svc/routes/order_price_list_export_notify.go @@ -0,0 +1,192 @@ +package routes + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "html" + "log" + "net/http" + "strings" + "time" + + "bssapp-backend/auth" + "bssapp-backend/internal/mailer" + + "github.com/lib/pq" +) + +type orderPriceListExportNotifyRequest struct { + Format string `json:"format"` + RowCount int `json:"row_count"` + PriceFields []string `json:"price_fields"` + ProductCodes []string `json:"product_codes"` + CampaignLabels []string `json:"campaign_labels"` + FirstGroups []string `json:"first_groups"` + UrunIlkGrubu string `json:"urun_ilk_grubu"` + UrunAnaGrubu string `json:"urun_ana_grubu"` +} + +func NotifyOrderPriceListExportHandler(pg *sql.DB, ml *mailer.GraphMailer) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + if ml == nil { + http.Error(w, "mailer unavailable", http.StatusServiceUnavailable) + return + } + if err := ensureFirstGroupMailMappingTables(pg); err != nil { + http.Error(w, "mapping table bootstrap error", http.StatusInternalServerError) + return + } + + var req orderPriceListExportNotifyRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid payload", http.StatusBadRequest) + return + } + + groups := normalizeExportNotifyList(req.FirstGroups, 100) + if len(groups) == 0 { + groups = normalizeExportNotifyList([]string{req.UrunIlkGrubu}, 100) + } + recipients, err := loadOrderPriceListRecipients(pg, groups) + if err != nil { + http.Error(w, "recipient lookup error", http.StatusInternalServerError) + return + } + if len(recipients) == 0 { + log.Printf("[order-price-list-export-notify] no recipients groups=%v", groups) + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "sent": false, "reason": "no_recipients"}) + return + } + + actor := strings.TrimSpace(claims.Username) + if actor == "" { + actor = fmt.Sprintf("user-%d", claims.ID) + } + format := strings.ToUpper(strings.TrimSpace(req.Format)) + if format == "" { + format = "CIKTI" + } + now := time.Now() + htmlBody := buildOrderPriceListExportNotifyHTML(req, actor, format, groups, now) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + if err := ml.Send(ctx, mailer.Message{ + To: recipients, + Subject: fmt.Sprintf("Fiyat Listesi Ciktisi Alindi | %s | %s", actor, now.Format("02.01.2006 15:04")), + BodyHTML: htmlBody, + }); err != nil { + log.Printf("[order-price-list-export-notify] send failed user=%s format=%s err=%v", actor, format, err) + http.Error(w, "mail send error", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "sent": true, "recipient_count": len(recipients)}) + } +} + +func loadOrderPriceListRecipients(pg *sql.DB, groups []string) ([]string, error) { + groups = normalizeExportNotifyList(groups, 100) + if len(groups) == 0 { + return nil, nil + } + rows, err := pg.Query(` +SELECT DISTINCT TRIM(m.email) AS email +FROM mk_order_price_list_first_group_mail f +JOIN mk_mail m + ON m.id = f.mail_id +WHERE m.is_active = true + AND COALESCE(TRIM(m.email), '') <> '' + AND UPPER(TRIM(f.urun_ilk_grubu)) = ANY($1) +ORDER BY email +`, pq.Array(upperExportNotifyList(groups))) + if err != nil { + return nil, err + } + defer rows.Close() + + out := make([]string, 0, 16) + for rows.Next() { + var email string + if err := rows.Scan(&email); err != nil { + return nil, err + } + email = strings.TrimSpace(email) + if email != "" { + out = append(out, email) + } + } + return out, rows.Err() +} + +func buildOrderPriceListExportNotifyHTML(req orderPriceListExportNotifyRequest, actor string, format string, groups []string, now time.Time) string { + return fmt.Sprintf(` +

Fiyat Listesi Ciktisi Alindi

+ + + + + + + + + + +
Islem Yapan%s
Cikti Tipi%s
Tarih%s
Satir Sayisi%d
Secili Fiyat Gruplari%s
Urun Ilk Gruplari%s
Urun Ana Grubu%s
Urun Kodlari%s
Kampanya Filtreleri%s
`, + html.EscapeString(actor), + html.EscapeString(format), + html.EscapeString(now.Format("02.01.2006 15:04")), + req.RowCount, + html.EscapeString(strings.Join(normalizeExportNotifyList(req.PriceFields, 24), ", ")), + html.EscapeString(strings.Join(groups, ", ")), + html.EscapeString(orderNotifyDash(req.UrunAnaGrubu)), + html.EscapeString(strings.Join(normalizeExportNotifyList(req.ProductCodes, 50), ", ")), + html.EscapeString(strings.Join(normalizeExportNotifyList(req.CampaignLabels, 20), ", ")), + ) +} + +func normalizeExportNotifyList(in []string, limit int) []string { + out := make([]string, 0, len(in)) + seen := make(map[string]struct{}, len(in)) + for _, raw := range in { + v := strings.TrimSpace(raw) + if v == "" || v == "-" { + continue + } + key := strings.ToUpper(v) + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + out = append(out, v) + if limit > 0 && len(out) >= limit { + break + } + } + return out +} + +func upperExportNotifyList(in []string) []string { + out := make([]string, 0, len(in)) + for _, v := range normalizeExportNotifyList(in, 0) { + out = append(out, strings.ToUpper(strings.TrimSpace(v))) + } + return out +} + +func orderNotifyDash(v string) string { + v = strings.TrimSpace(v) + if v == "" { + return "-" + } + return v +} diff --git a/ui/src/layouts/MainLayout.vue b/ui/src/layouts/MainLayout.vue index 85cc30f..6a79a16 100644 --- a/ui/src/layouts/MainLayout.vue +++ b/ui/src/layouts/MainLayout.vue @@ -413,6 +413,11 @@ const menuItems = [ label: 'Fiyatlandırma Mail Eşleştirme', to: '/app/pricing-mail-mapping', permission: 'system:update' + }, + { + label: 'Fiyat Listesi Mail Eşleştirme', + to: '/app/order-price-list-mail-mapping', + permission: 'system:update' } ] diff --git a/ui/src/pages/OrderPriceList.vue b/ui/src/pages/OrderPriceList.vue index 5699016..89eb014 100644 --- a/ui/src/pages/OrderPriceList.vue +++ b/ui/src/pages/OrderPriceList.vue @@ -521,6 +521,7 @@ const serverFilterLastQuery = ref({}) const filterSearch = ref({ productCode: '', urunIlkGrubu: '', urunAnaGrubu: '' }) const imageCache = new Map() const imageListCache = new Map() +const variantCodeCollator = new Intl.Collator('tr', { numeric: true, sensitivity: 'base' }) const mainTableRef = ref(null) const topScrollRef = ref(null) let syncingScroll = false @@ -648,7 +649,7 @@ function buildRows (products, variants) { out.push(row) continue } - list.sort((a, b) => toText(a?.variant_code).localeCompare(toText(b?.variant_code), 'tr')) + list.sort((a, b) => variantCodeCollator.compare(toText(a?.variant_code), toText(b?.variant_code))) for (const v of list) { const d1 = Number(v?.dim1 || 0) const d3 = v?.dim3 == null ? 0 : Number(v?.dim3 || 0) @@ -991,17 +992,17 @@ const allColumns = [ col('brandGroupSelection', 'MARKA GRUBU', 'brandGroupSelection', 86, { classes: 'ps-col sticky-col' }), col('marka', 'MARKA', 'marka', 72, { sortable: true, classes: 'ps-col sticky-col' }), col('productCode', 'URUN KODU', 'productCode', 112, { sortable: true, classes: 'ps-col product-code-col sticky-col' }), - col('variantCodes', 'VARYANT', 'variantCodes', 128, { classes: 'ps-col variant-col sticky-col' }), - col('variantStocks', 'STOK', 'stockQty', 72, { align: 'right', sortable: true, classes: 'ps-col variant-stock-col sticky-col' }), - col('campaignLabel', 'KAMPANYA', 'campaignLabel', 150, { classes: 'ps-col campaign-col sticky-col' }), - col('campaignRate', 'IND %', 'campaignRate', 64, { align: 'right', classes: 'ps-col campaign-rate-col sticky-col' }), - col('askiliYan', 'ASKILI YAN', 'askiliYan', 72, { sortable: true, classes: 'ps-col' }), - col('kategori', 'KATEGORI', 'kategori', 82, { sortable: true, classes: 'ps-col' }), - col('urunIlkGrubu', 'URUN ILK GRUBU', 'urunIlkGrubu', 88, { sortable: true, classes: 'ps-col' }), - col('urunAnaGrubu', 'URUN ANA GRUBU', 'urunAnaGrubu', 96, { sortable: true, classes: 'ps-col' }), - col('urunAltGrubu', 'URUN ALT GRUBU', 'urunAltGrubu', 96, { sortable: true, classes: 'ps-col' }), - col('icerik', 'ICERIK', 'icerik', 112, { sortable: true, classes: 'ps-col' }), - col('karisim', 'KARISIM', 'karisim', 96, { sortable: true, classes: 'ps-col' }), + col('variantCodes', 'VARYANT', 'variantCodes', 82, { align: 'center', classes: 'ps-col variant-col sticky-col center-col' }), + col('variantStocks', 'STOK', 'stockQty', 64, { align: 'center', sortable: true, classes: 'ps-col variant-stock-col sticky-col center-col' }), + col('campaignLabel', 'KAMPANYA', 'campaignLabel', 118, { align: 'center', classes: 'ps-col campaign-col sticky-col center-col' }), + col('campaignRate', 'IND %', 'campaignRate', 56, { align: 'center', classes: 'ps-col campaign-rate-col sticky-col center-col' }), + col('askiliYan', 'ASKILI YAN', 'askiliYan', 58, { sortable: true, classes: 'ps-col center-col' }), + col('kategori', 'KATEGORI', 'kategori', 72, { sortable: true, classes: 'ps-col center-col' }), + col('urunIlkGrubu', 'URUN ILK GRUBU', 'urunIlkGrubu', 72, { sortable: true, classes: 'ps-col center-col' }), + col('urunAnaGrubu', 'URUN ANA GRUBU', 'urunAnaGrubu', 84, { sortable: true, classes: 'ps-col center-col' }), + col('urunAltGrubu', 'URUN ALT GRUBU', 'urunAltGrubu', 84, { sortable: true, classes: 'ps-col center-col' }), + col('icerik', 'ICERIK', 'icerik', 92, { sortable: true, classes: 'ps-col' }), + col('karisim', 'KARISIM', 'karisim', 88, { sortable: true, classes: 'ps-col karisim-wrap-col' }), ...campaignPairs.flatMap((p) => [ col(p.base, p.base.toUpperCase().replace(/([A-Z]+)(\d)/, '$1 $2'), p.base, 78, { align: 'right', classes: `${p.base.slice(0, 3)}-col` }), col(p.derived, `${p.base.toUpperCase().replace(/([A-Z]+)(\d)/, '$1 $2')} KMP`, p.derived, 88, { align: 'right', classes: `${p.base.slice(0, 3)}-col campaign-price-col` }) @@ -1131,6 +1132,7 @@ function exportVisibleExcel () { a.click() a.remove() URL.revokeObjectURL(url) + void notifyExportTaken('excel') } function printVisibleRows () { @@ -1154,6 +1156,24 @@ function printVisibleRows () { win.document.open() win.document.write(html) win.document.close() + void notifyExportTaken('pdf') +} + +async function notifyExportTaken (format) { + try { + await api.post('/order/price-list/export-notify', { + format, + row_count: filteredRows.value.length, + price_fields: [...selectedPriceOptions.value], + product_codes: [...selectedProductCodes.value], + campaign_labels: [...selectedCampaignLabels.value], + first_groups: Array.from(new Set(filteredRows.value.map((row) => toText(row.urunIlkGrubu)).filter(Boolean))).sort((a, b) => a.localeCompare(b, 'tr')), + urun_ilk_grubu: topUrunIlkGrubu.value || '', + urun_ana_grubu: topUrunAnaGrubu.value || '' + }, { timeout: 30000 }) + } catch (err) { + console.warn('[order-price-list][ui] export notify failed', err?.response?.data || err?.message || err) + } } function getTableMiddleEl () { @@ -1435,6 +1455,36 @@ onMounted(() => { vertical-align: middle !important; } +.pricing-table :deep(td.center-col) { + text-align: center !important; +} + +.pricing-table :deep(td.center-col .q-badge) { + margin-left: auto; + margin-right: auto; +} + +.pricing-table :deep(td.karisim-wrap-col) { + white-space: normal !important; + text-overflow: clip !important; + word-break: break-word; + overflow-wrap: anywhere; + vertical-align: top !important; + text-align: left !important; + font-size: 9px; + line-height: 1.15; + padding: 6px 4px !important; + align-content: flex-start; +} + +.pricing-table :deep(td.karisim-wrap-col .q-td__content), +.pricing-table :deep(td.karisim-wrap-col > div) { + align-items: flex-start !important; + justify-content: flex-start !important; + white-space: normal !important; + overflow: hidden; +} + .order-price-list-table :deep(.campaign-price-col) { background: #f6fbf7; } diff --git a/ui/src/pages/OrderPriceListMailMapping.vue b/ui/src/pages/OrderPriceListMailMapping.vue new file mode 100644 index 0000000..dbdaa8c --- /dev/null +++ b/ui/src/pages/OrderPriceListMailMapping.vue @@ -0,0 +1,172 @@ + + + diff --git a/ui/src/router/routes.js b/ui/src/router/routes.js index eb40dda..9baf52d 100644 --- a/ui/src/router/routes.js +++ b/ui/src/router/routes.js @@ -251,6 +251,12 @@ const routes = [ component: () => import('../pages/PricingMailMapping.vue'), meta: { permission: 'system:update' } }, + { + path: 'order-price-list-mail-mapping', + name: 'order-price-list-mail-mapping', + component: () => import('../pages/OrderPriceListMailMapping.vue'), + meta: { permission: 'system:update' } + }, { path: 'language/translations', name: 'translation-table', diff --git a/ui/src/stores/orderPriceListMailMappingStore.js b/ui/src/stores/orderPriceListMailMappingStore.js new file mode 100644 index 0000000..e093c61 --- /dev/null +++ b/ui/src/stores/orderPriceListMailMappingStore.js @@ -0,0 +1,47 @@ +import { defineStore } from 'pinia' +import api from 'src/services/api' + +export const useOrderPriceListMailMappingStore = defineStore('orderPriceListMailMapping', { + state: () => ({ + loading: false, + saving: false, + firstGroups: [], + mails: [], + rows: [] + }), + + actions: { + async fetchLookups () { + this.loading = true + try { + const res = await api.get('/system/order-price-list-mail-mappings/lookups') + const payload = res?.data || {} + this.firstGroups = Array.isArray(payload.first_groups) ? payload.first_groups : [] + this.mails = Array.isArray(payload.mails) ? payload.mails : [] + } finally { + this.loading = false + } + }, + + async fetchRows () { + this.loading = true + try { + const res = await api.get('/system/order-price-list-mail-mappings') + this.rows = Array.isArray(res?.data) ? res.data : [] + } finally { + this.loading = false + } + }, + + async saveGroupMails (urunIlkGrubu, mailIds) { + this.saving = true + try { + await api.put(`/system/order-price-list-mail-mappings/${encodeURIComponent(String(urunIlkGrubu || '').trim())}`, { + mail_ids: Array.isArray(mailIds) ? mailIds : [] + }) + } finally { + this.saving = false + } + } + } +})