diff --git a/svc/main.go b/svc/main.go index 90c8479..02823d2 100644 --- a/svc/main.go +++ b/svc/main.go @@ -245,6 +245,22 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router wrapV3(routes.TestMailHandler(ml)), ) + bindV3(r, pgDB, + "/api/system/market-mail-mappings/lookups", "GET", + "system", "update", + wrapV3(routes.GetMarketMailMappingLookupsHandler(pgDB)), + ) + bindV3(r, pgDB, + "/api/system/market-mail-mappings", "GET", + "system", "update", + wrapV3(routes.GetMarketMailMappingsHandler(pgDB)), + ) + bindV3(r, pgDB, + "/api/system/market-mail-mappings/{marketId}", "PUT", + "system", "update", + wrapV3(routes.SaveMarketMailMappingHandler(pgDB)), + ) + // ============================================================ // PERMISSIONS // ============================================================ diff --git a/svc/models/market_mail_mapping.go b/svc/models/market_mail_mapping.go new file mode 100644 index 0000000..e0728a8 --- /dev/null +++ b/svc/models/market_mail_mapping.go @@ -0,0 +1,26 @@ +package models + +type MarketMailOption struct { + ID string `json:"id"` + Label string `json:"label"` +} + +type MarketOption struct { + ID int64 `json:"id"` + Code string `json:"code"` + Title string `json:"title"` +} + +type MailOption struct { + ID string `json:"id"` + Email string `json:"email"` + DisplayName string `json:"display_name"` +} + +type MarketMailMappingRow struct { + MarketID int64 `json:"market_id"` + MarketCode string `json:"market_code"` + MarketTitle string `json:"market_title"` + MailIDs []string `json:"mail_ids"` + Mails []MarketMailOption `json:"mails"` +} diff --git a/svc/queries/market_mail_mapping.go b/svc/queries/market_mail_mapping.go new file mode 100644 index 0000000..6758532 --- /dev/null +++ b/svc/queries/market_mail_mapping.go @@ -0,0 +1,67 @@ +package queries + +const GetActiveMarketsForMapping = ` +SELECT + p.id, + p.code, + p.title +FROM mk_sales_piy p +WHERE p.is_active = true +ORDER BY p.title, p.code +` + +const GetActiveMailsForMapping = ` +SELECT + m.id::text, + m.email, + COALESCE(NULLIF(m.display_name, ''), m.email) AS display_name +FROM mk_mail m +WHERE m.is_active = true +ORDER BY m.email +` + +const GetMarketMailMappingRows = ` +SELECT + p.id, + p.code, + p.title, + m.id::text, + m.email, + COALESCE(NULLIF(m.display_name, ''), m.email) AS display_name +FROM mk_sales_piy p +LEFT JOIN mk_market_mail mm + ON mm.market_id = p.id +LEFT JOIN mk_mail m + ON m.id = mm.mail_id + AND m.is_active = true +WHERE p.is_active = true +ORDER BY p.title, p.code, m.email +` + +const ExistsActiveMarketByID = ` +SELECT EXISTS ( + SELECT 1 + FROM mk_sales_piy p + WHERE p.id = $1 + AND p.is_active = true +) +` + +const ExistsActiveMailByID = ` +SELECT EXISTS ( + SELECT 1 + FROM mk_mail m + WHERE m.id = $1 + AND m.is_active = true +) +` + +const DeleteMarketMailsByMarketID = ` +DELETE FROM mk_market_mail +WHERE market_id = $1 +` + +const InsertMarketMailMapping = ` +INSERT INTO mk_market_mail (market_id, mail_id) +VALUES ($1, $2) +` diff --git a/svc/routes/market_mail_mapping.go b/svc/routes/market_mail_mapping.go new file mode 100644 index 0000000..5e7f84c --- /dev/null +++ b/svc/routes/market_mail_mapping.go @@ -0,0 +1,244 @@ +package routes + +import ( + "bssapp-backend/models" + "bssapp-backend/queries" + "database/sql" + "encoding/json" + "net/http" + "sort" + "strconv" + "strings" + + "github.com/gorilla/mux" +) + +type MarketMailSavePayload struct { + MailIDs []string `json:"mail_ids"` +} + +type MarketMailLookupResponse struct { + Markets []models.MarketOption `json:"markets"` + Mails []models.MailOption `json:"mails"` +} + +func GetMarketMailMappingLookupsHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + markets := make([]models.MarketOption, 0, 64) + mails := make([]models.MailOption, 0, 128) + + marketRows, err := db.Query(queries.GetActiveMarketsForMapping) + if err != nil { + http.Error(w, "markets lookup error", http.StatusInternalServerError) + return + } + defer marketRows.Close() + + for marketRows.Next() { + var item models.MarketOption + if err := marketRows.Scan(&item.ID, &item.Code, &item.Title); err != nil { + http.Error(w, "markets scan error", http.StatusInternalServerError) + return + } + markets = append(markets, item) + } + if err := marketRows.Err(); err != nil { + http.Error(w, "markets rows error", http.StatusInternalServerError) + return + } + + mailRows, err := db.Query(queries.GetActiveMailsForMapping) + if err != nil { + http.Error(w, "mails lookup error", http.StatusInternalServerError) + return + } + defer mailRows.Close() + + for mailRows.Next() { + var item models.MailOption + if err := mailRows.Scan(&item.ID, &item.Email, &item.DisplayName); err != nil { + http.Error(w, "mails scan error", http.StatusInternalServerError) + return + } + mails = append(mails, item) + } + if err := mailRows.Err(); err != nil { + http.Error(w, "mails rows error", http.StatusInternalServerError) + return + } + + _ = json.NewEncoder(w).Encode(MarketMailLookupResponse{ + Markets: markets, + Mails: mails, + }) + } +} + +func GetMarketMailMappingsHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + rows, err := db.Query(queries.GetMarketMailMappingRows) + if err != nil { + http.Error(w, "mapping query error", http.StatusInternalServerError) + return + } + defer rows.Close() + + byMarket := make(map[int64]*models.MarketMailMappingRow, 64) + order := make([]int64, 0, 64) + + for rows.Next() { + var marketID int64 + var marketCode, marketTitle string + var mailID sql.NullString + var email sql.NullString + var displayName sql.NullString + + if err := rows.Scan( + &marketID, + &marketCode, + &marketTitle, + &mailID, + &email, + &displayName, + ); err != nil { + http.Error(w, "mapping scan error", http.StatusInternalServerError) + return + } + + row, ok := byMarket[marketID] + if !ok { + row = &models.MarketMailMappingRow{ + MarketID: marketID, + MarketCode: marketCode, + MarketTitle: marketTitle, + MailIDs: make([]string, 0, 8), + Mails: make([]models.MarketMailOption, 0, 8), + } + byMarket[marketID] = row + order = append(order, marketID) + } + + 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.MarketMailOption{ + ID: id, + Label: label, + }) + } + } + if err := rows.Err(); err != nil { + http.Error(w, "mapping rows error", http.StatusInternalServerError) + return + } + + list := make([]models.MarketMailMappingRow, 0, len(order)) + for _, marketID := range order { + list = append(list, *byMarket[marketID]) + } + + _ = json.NewEncoder(w).Encode(list) + } +} + +func SaveMarketMailMappingHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + marketIDStr := mux.Vars(r)["marketId"] + marketID, err := strconv.ParseInt(marketIDStr, 10, 64) + if err != nil || marketID <= 0 { + http.Error(w, "invalid market id", http.StatusBadRequest) + return + } + + var payload MarketMailSavePayload + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, "invalid payload", http.StatusBadRequest) + return + } + + var marketExists bool + if err := db.QueryRow(queries.ExistsActiveMarketByID, marketID).Scan(&marketExists); err != nil { + http.Error(w, "market validate error", http.StatusInternalServerError) + return + } + if !marketExists { + http.Error(w, "market not found", http.StatusNotFound) + return + } + + mailIDs := normalizeIDList(payload.MailIDs) + for _, mailID := range mailIDs { + var mailExists bool + if err := db.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 := db.Begin() + if err != nil { + http.Error(w, "transaction start error", http.StatusInternalServerError) + return + } + defer tx.Rollback() + + if _, err := tx.Exec(queries.DeleteMarketMailsByMarketID, marketID); err != nil { + http.Error(w, "mapping delete error", http.StatusInternalServerError) + return + } + + for _, mailID := range mailIDs { + if _, err := tx.Exec(queries.InsertMarketMailMapping, marketID, 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, + "market_id": marketID, + "mail_ids": mailIDs, + }) + } +} + +func normalizeIDList(ids []string) []string { + seen := make(map[string]struct{}, len(ids)) + out := make([]string, 0, len(ids)) + + for _, raw := range ids { + id := strings.TrimSpace(raw) + if id == "" { + continue + } + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + out = append(out, id) + } + + sort.Strings(out) + return out +} diff --git a/ui/quasar.config.js.temporary.compiled.1773240229507.mjs b/ui/quasar.config.js.temporary.compiled.1773759477667.mjs similarity index 100% rename from ui/quasar.config.js.temporary.compiled.1773240229507.mjs rename to ui/quasar.config.js.temporary.compiled.1773759477667.mjs diff --git a/ui/src/layouts/MainLayout.vue b/ui/src/layouts/MainLayout.vue index 6979f7b..5daa134 100644 --- a/ui/src/layouts/MainLayout.vue +++ b/ui/src/layouts/MainLayout.vue @@ -307,6 +307,12 @@ const menuItems = [ label: 'Test Mail', to: '/app/test-mail', permission: 'system:update' + }, + + { + label: 'Piyasa Mail Eşleştirme', + to: '/app/market-mail-mapping', + permission: 'system:update' } ] diff --git a/ui/src/pages/AccountAgingStatement.vue b/ui/src/pages/AccountAgingStatement.vue index 0876c1c..578a9de 100644 --- a/ui/src/pages/AccountAgingStatement.vue +++ b/ui/src/pages/AccountAgingStatement.vue @@ -444,6 +444,15 @@ function formatAmount(value, fraction = 2) {