From f93041241374b452fa2eba1a64166c7e9f7e65ab Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Thu, 21 May 2026 10:55:20 +0300 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- .../2026-05-21_first_group_mail_mappings.sql | 17 + svc/main.go | 32 ++ svc/models/first_group_mail_mapping.go | 17 + svc/queries/first_group_mail_mapping.go | 47 ++ svc/queries/product_first_group.go | 34 ++ svc/routes/first_group_mail_mapping.go | 418 ++++++++++++++++++ svc/routes/production_product_costing_pdf.go | 16 +- ui/.quasar/prod-spa/app.js | 75 ---- ui/.quasar/prod-spa/client-entry.js | 158 ------- ui/.quasar/prod-spa/client-prefetch.js | 116 ----- ui/.quasar/prod-spa/quasar-user-options.js | 23 - ...g.js.temporary.compiled.1779349901589.mjs} | 0 ui/src/layouts/MainLayout.vue | 10 + ui/src/pages/CostingMailMapping.vue | 172 +++++++ ui/src/pages/PricingMailMapping.vue | 172 +++++++ ui/src/router/routes.js | 12 + ui/src/stores/costingMailMappingStore.js | 48 ++ ui/src/stores/pricingMailMappingStore.js | 48 ++ 18 files changed, 1041 insertions(+), 374 deletions(-) create mode 100644 svc/db/migrations/2026-05-21_first_group_mail_mappings.sql create mode 100644 svc/models/first_group_mail_mapping.go create mode 100644 svc/queries/first_group_mail_mapping.go create mode 100644 svc/queries/product_first_group.go create mode 100644 svc/routes/first_group_mail_mapping.go delete mode 100644 ui/.quasar/prod-spa/app.js delete mode 100644 ui/.quasar/prod-spa/client-entry.js delete mode 100644 ui/.quasar/prod-spa/client-prefetch.js delete mode 100644 ui/.quasar/prod-spa/quasar-user-options.js rename ui/{quasar.config.js.temporary.compiled.1779296132484.mjs => quasar.config.js.temporary.compiled.1779349901589.mjs} (100%) create mode 100644 ui/src/pages/CostingMailMapping.vue create mode 100644 ui/src/pages/PricingMailMapping.vue create mode 100644 ui/src/stores/costingMailMappingStore.js create mode 100644 ui/src/stores/pricingMailMappingStore.js diff --git a/svc/db/migrations/2026-05-21_first_group_mail_mappings.sql b/svc/db/migrations/2026-05-21_first_group_mail_mappings.sql new file mode 100644 index 0000000..f092430 --- /dev/null +++ b/svc/db/migrations/2026-05-21_first_group_mail_mappings.sql @@ -0,0 +1,17 @@ +-- Product First Group mail mappings (Postgres) +-- Two independent mapping tables: +-- 1) Costing mails: triggered when a costing is created/updated +-- 2) Pricing mails: triggered when pricing is created/updated + +CREATE TABLE IF NOT EXISTS mk_costing_first_group_mail ( + urun_ilk_grubu TEXT NOT NULL, + mail_id BIGINT NOT NULL REFERENCES mk_mail(id) ON DELETE CASCADE, + PRIMARY KEY (urun_ilk_grubu, mail_id) +); + +CREATE TABLE IF NOT EXISTS mk_pricing_first_group_mail ( + urun_ilk_grubu TEXT NOT NULL, + mail_id BIGINT NOT NULL REFERENCES mk_mail(id) ON DELETE CASCADE, + PRIMARY KEY (urun_ilk_grubu, mail_id) +); + diff --git a/svc/main.go b/svc/main.go index f58d363..823bd34 100644 --- a/svc/main.go +++ b/svc/main.go @@ -321,6 +321,38 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "system", "update", wrapV3(routes.SaveMarketMailMappingHandler(pgDB)), ) + + bindV3(r, pgDB, + "/api/system/costing-mail-mappings/lookups", "GET", + "system", "update", + wrapV3(routes.GetCostingFirstGroupMailMappingLookupsHandler(pgDB)), + ) + bindV3(r, pgDB, + "/api/system/costing-mail-mappings", "GET", + "system", "update", + wrapV3(routes.GetCostingFirstGroupMailMappingsHandler(pgDB)), + ) + bindV3(r, pgDB, + "/api/system/costing-mail-mappings/{group}", "PUT", + "system", "update", + wrapV3(routes.SaveCostingFirstGroupMailMappingHandler(pgDB)), + ) + + bindV3(r, pgDB, + "/api/system/pricing-mail-mappings/lookups", "GET", + "system", "update", + wrapV3(routes.GetPricingFirstGroupMailMappingLookupsHandler(pgDB)), + ) + bindV3(r, pgDB, + "/api/system/pricing-mail-mappings", "GET", + "system", "update", + wrapV3(routes.GetPricingFirstGroupMailMappingsHandler(pgDB)), + ) + bindV3(r, pgDB, + "/api/system/pricing-mail-mappings/{group}", "PUT", + "system", "update", + wrapV3(routes.SavePricingFirstGroupMailMappingHandler(pgDB)), + ) bindV3(r, pgDB, "/api/language/translations", "GET", "language", "update", diff --git a/svc/models/first_group_mail_mapping.go b/svc/models/first_group_mail_mapping.go new file mode 100644 index 0000000..ee8f821 --- /dev/null +++ b/svc/models/first_group_mail_mapping.go @@ -0,0 +1,17 @@ +package models + +type FirstGroupOption struct { + ID string `json:"id"` + Label string `json:"label"` +} + +type FirstGroupMailOption struct { + ID string `json:"id"` + Label string `json:"label"` +} + +type FirstGroupMailMappingRow struct { + UrunIlkGrubu string `json:"urun_ilk_grubu"` + MailIDs []string `json:"mail_ids"` + Mails []FirstGroupMailOption `json:"mails"` +} diff --git a/svc/queries/first_group_mail_mapping.go b/svc/queries/first_group_mail_mapping.go new file mode 100644 index 0000000..a6dd9b2 --- /dev/null +++ b/svc/queries/first_group_mail_mapping.go @@ -0,0 +1,47 @@ +package queries + +const GetCostingFirstGroupMailMappingRows = ` +SELECT + f.urun_ilk_grubu, + m.id::text, + m.email, + COALESCE(NULLIF(m.display_name, ''), m.email) AS display_name +FROM mk_costing_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 DeleteCostingFirstGroupMailsByGroup = ` +DELETE FROM mk_costing_first_group_mail +WHERE urun_ilk_grubu = $1 +` + +const InsertCostingFirstGroupMailMapping = ` +INSERT INTO mk_costing_first_group_mail (urun_ilk_grubu, mail_id) +VALUES ($1, $2) +` + +const GetPricingFirstGroupMailMappingRows = ` +SELECT + f.urun_ilk_grubu, + m.id::text, + m.email, + COALESCE(NULLIF(m.display_name, ''), m.email) AS display_name +FROM mk_pricing_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 DeletePricingFirstGroupMailsByGroup = ` +DELETE FROM mk_pricing_first_group_mail +WHERE urun_ilk_grubu = $1 +` + +const InsertPricingFirstGroupMailMapping = ` +INSERT INTO mk_pricing_first_group_mail (urun_ilk_grubu, mail_id) +VALUES ($1, $2) +` diff --git a/svc/queries/product_first_group.go b/svc/queries/product_first_group.go new file mode 100644 index 0000000..2368e72 --- /dev/null +++ b/svc/queries/product_first_group.go @@ -0,0 +1,34 @@ +package queries + +import ( + "context" + "database/sql" + "strings" +) + +// ListProductFirstGroupOptions returns distinct ProductAtt42Desc (UrunIlkGrubu) values from Nebim V3. +func ListProductFirstGroupOptions(ctx context.Context, mssqlDB *sql.DB, search string, limit int) (*sql.Rows, error) { + search = strings.TrimSpace(search) + if mssqlDB == nil { + return nil, sql.ErrConnDone + } + if limit <= 0 || limit > 5000 { + limit = 5000 + } + + sqlText := ` +SELECT TOP (@p2) + LTRIM(RTRIM(ISNULL(ProductAtt42Desc, ''))) AS urun_ilk_grubu +FROM ProductFilterWithDescription('TR') +WHERE IsBlocked = 0 + AND LTRIM(RTRIM(ISNULL(ProductAtt42Desc, ''))) <> '' + AND ( + @p1 = '' + OR LTRIM(RTRIM(ISNULL(ProductAtt42Desc, ''))) LIKE '%' + @p1 + '%' + ) +GROUP BY LTRIM(RTRIM(ISNULL(ProductAtt42Desc, ''))) +ORDER BY LTRIM(RTRIM(ISNULL(ProductAtt42Desc, ''))); +` + + return mssqlDB.QueryContext(ctx, sqlText, search, limit) +} diff --git a/svc/routes/first_group_mail_mapping.go b/svc/routes/first_group_mail_mapping.go new file mode 100644 index 0000000..e3e6ed5 --- /dev/null +++ b/svc/routes/first_group_mail_mapping.go @@ -0,0 +1,418 @@ +package routes + +import ( + "bssapp-backend/db" + "bssapp-backend/models" + "bssapp-backend/queries" + "bssapp-backend/utils" + "database/sql" + "encoding/json" + "net/http" + "sort" + "strings" + + "github.com/gorilla/mux" +) + +type FirstGroupMailSavePayload struct { + MailIDs []string `json:"mail_ids"` +} + +type FirstGroupMailLookupResponse struct { + FirstGroups []models.FirstGroupOption `json:"first_groups"` + Mails []models.MailOption `json:"mails"` +} + +func GetCostingFirstGroupMailMappingLookupsHandler(pg *sql.DB) 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 + } + + traceID := utils.TraceIDFromRequest(r) + ctx := utils.ContextWithTraceID(r.Context(), traceID) + + firstGroups := make([]models.FirstGroupOption, 0, 256) + mails := make([]models.MailOption, 0, 256) + + fgRows, err := queries.ListProductFirstGroupOptions(ctx, mssql, "", 5000) + if err != nil { + http.Error(w, "first group lookup error", http.StatusInternalServerError) + return + } + defer fgRows.Close() + for fgRows.Next() { + var g string + if err := fgRows.Scan(&g); err != nil { + http.Error(w, "first group scan error", http.StatusInternalServerError) + return + } + g = strings.TrimSpace(g) + if g != "" { + firstGroups = append(firstGroups, models.FirstGroupOption{ID: g, Label: g}) + } + } + if err := fgRows.Err(); err != nil { + http.Error(w, "first group rows error", http.StatusInternalServerError) + return + } + + mailRows, err := pg.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(FirstGroupMailLookupResponse{FirstGroups: firstGroups, Mails: mails}) + } +} + +func GetPricingFirstGroupMailMappingLookupsHandler(pg *sql.DB) http.HandlerFunc { + // same lookups as costing + 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") + + mssql := db.GetDB() + if mssql == nil { + http.Error(w, "mssql connection not available", http.StatusServiceUnavailable) + return + } + + traceID := utils.TraceIDFromRequest(r) + ctx := utils.ContextWithTraceID(r.Context(), traceID) + + // Fetch all first groups from V3 (source of truth for the list) + allGroups := make([]string, 0, 512) + fgRows, err := queries.ListProductFirstGroupOptions(ctx, mssql, "", 5000) + if err != nil { + http.Error(w, "first group lookup error", http.StatusInternalServerError) + return + } + defer fgRows.Close() + for fgRows.Next() { + var g string + if err := fgRows.Scan(&g); err != nil { + http.Error(w, "first group scan error", http.StatusInternalServerError) + return + } + g = strings.TrimSpace(g) + if g != "" { + allGroups = append(allGroups, g) + } + } + if err := fgRows.Err(); err != nil { + http.Error(w, "first group rows error", http.StatusInternalServerError) + return + } + sort.Strings(allGroups) + + // Fetch mappings from Postgres + rows, err := pg.Query(queries.GetCostingFirstGroupMailMappingRows) + if err != nil { + http.Error(w, "mapping query error", http.StatusInternalServerError) + return + } + defer rows.Close() + + byGroup := map[string]*models.FirstGroupMailMappingRow{} + for _, g := range allGroups { + byGroup[g] = &models.FirstGroupMailMappingRow{ + UrunIlkGrubu: g, + 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 + } + g := strings.TrimSpace(group.String) + if g == "" { + continue + } + row, ok := byGroup[g] + if !ok { + row = &models.FirstGroupMailMappingRow{ + UrunIlkGrubu: g, + MailIDs: make([]string, 0, 8), + Mails: make([]models.FirstGroupMailOption, 0, 8), + } + byGroup[g] = row + allGroups = append(allGroups, g) + } + 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 + } + + sort.Strings(allGroups) + out := make([]models.FirstGroupMailMappingRow, 0, len(allGroups)) + for _, g := range allGroups { + if r := byGroup[g]; r != nil { + r.MailIDs = normalizeIDList(r.MailIDs) + out = append(out, *r) + } + } + _ = json.NewEncoder(w).Encode(out) + } +} + +func GetPricingFirstGroupMailMappingsHandler(pg *sql.DB) 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 + } + + traceID := utils.TraceIDFromRequest(r) + ctx := utils.ContextWithTraceID(r.Context(), traceID) + + allGroups := make([]string, 0, 512) + fgRows, err := queries.ListProductFirstGroupOptions(ctx, mssql, "", 5000) + if err != nil { + http.Error(w, "first group lookup error", http.StatusInternalServerError) + return + } + defer fgRows.Close() + for fgRows.Next() { + var g string + if err := fgRows.Scan(&g); err != nil { + http.Error(w, "first group scan error", http.StatusInternalServerError) + return + } + g = strings.TrimSpace(g) + if g != "" { + allGroups = append(allGroups, g) + } + } + if err := fgRows.Err(); err != nil { + http.Error(w, "first group rows error", http.StatusInternalServerError) + return + } + sort.Strings(allGroups) + + rows, err := pg.Query(queries.GetPricingFirstGroupMailMappingRows) + if err != nil { + http.Error(w, "mapping query error", http.StatusInternalServerError) + return + } + defer rows.Close() + + byGroup := map[string]*models.FirstGroupMailMappingRow{} + for _, g := range allGroups { + byGroup[g] = &models.FirstGroupMailMappingRow{ + UrunIlkGrubu: g, + 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 + } + g := strings.TrimSpace(group.String) + if g == "" { + continue + } + row, ok := byGroup[g] + if !ok { + row = &models.FirstGroupMailMappingRow{ + UrunIlkGrubu: g, + MailIDs: make([]string, 0, 8), + Mails: make([]models.FirstGroupMailOption, 0, 8), + } + byGroup[g] = row + allGroups = append(allGroups, g) + } + 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 + } + + sort.Strings(allGroups) + out := make([]models.FirstGroupMailMappingRow, 0, len(allGroups)) + for _, g := range allGroups { + if r := byGroup[g]; r != nil { + r.MailIDs = normalizeIDList(r.MailIDs) + out = append(out, *r) + } + } + _ = json.NewEncoder(w).Encode(out) + } +} + +func SaveCostingFirstGroupMailMappingHandler(pg *sql.DB) 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(queries.DeleteCostingFirstGroupMailsByGroup, group); err != nil { + http.Error(w, "mapping delete error", http.StatusInternalServerError) + return + } + for _, mailID := range mailIDs { + if _, err := tx.Exec(queries.InsertCostingFirstGroupMailMapping, 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, + }) + } +} + +func SavePricingFirstGroupMailMappingHandler(pg *sql.DB) 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(queries.DeletePricingFirstGroupMailsByGroup, group); err != nil { + http.Error(w, "mapping delete error", http.StatusInternalServerError) + return + } + for _, mailID := range mailIDs { + if _, err := tx.Exec(queries.InsertPricingFirstGroupMailMapping, 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/production_product_costing_pdf.go b/svc/routes/production_product_costing_pdf.go index 37fc075..efce93b 100644 --- a/svc/routes/production_product_costing_pdf.go +++ b/svc/routes/production_product_costing_pdf.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "sort" + "strconv" "strings" "time" @@ -265,8 +266,9 @@ func (c *costingPDF) drawHeaderFull() { firmaLabel = fmt.Sprintf("%s - %s", firmaLabel, strings.TrimSpace(c.header.UretimiYapanFirma)) } line3 := fmt.Sprintf( - "Firma: %s | Kaydeden: %s | Son Guncelleme: %s (%s)", + "Firma: %s | 2.Firma: %s | Kaydeden: %s | Son Guncelleme: %s (%s)", firmaLabel, + strings.TrimSpace(c.header.SonIsEmriVeren), strings.TrimSpace(c.header.SKullaniciAdi), formatDateTRDot(c.header.DteGuncellemeTarihi), strings.TrimSpace(c.header.SGuncellemeKullaniciAdi), @@ -509,7 +511,17 @@ func (c *costingPDF) drawGroup(g models.ProductionHasCostDetailGroup, firstGroup wn := []float64{8, 20, 22, 32, 70, 14, 14, 10, 16, 16, 16, 16} // sum = 250 c.drawTableHeader(cols, wn) - for i, it := range g.Items { + // PDF-specific ordering: by hammadde turu no, then code. + items := append([]models.ProductionHasCostDetailGroupItem(nil), g.Items...) + sort.Slice(items, func(i, j int) bool { + ai, _ := strconv.Atoi(strings.TrimSpace(items[i].NHammaddeTuruNo)) + aj, _ := strconv.Atoi(strings.TrimSpace(items[j].NHammaddeTuruNo)) + if ai != aj { + return ai < aj + } + return strings.TrimSpace(items[i].SKodu) < strings.TrimSpace(items[j].SKodu) + }) + for i, it := range items { c.drawRowWithGroup(it, wn, cols, g, i) } pdf.Ln(2) diff --git a/ui/.quasar/prod-spa/app.js b/ui/.quasar/prod-spa/app.js deleted file mode 100644 index caeaac1..0000000 --- a/ui/.quasar/prod-spa/app.js +++ /dev/null @@ -1,75 +0,0 @@ -/* eslint-disable */ -/** - * THIS FILE IS GENERATED AUTOMATICALLY. - * DO NOT EDIT. - * - * You are probably looking on adding startup/initialization code. - * Use "quasar new boot " and add it there. - * One boot file per concern. Then reference the file(s) in quasar.config file > boot: - * boot: ['file', ...] // do not add ".js" extension to it. - * - * Boot files are your "main.js" - **/ - - - - - -import { Quasar } from 'quasar' -import { markRaw } from 'vue' -import RootComponent from 'app/src/App.vue' - -import createStore from 'app/src/stores/index' -import createRouter from 'app/src/router/index' - - - - - -export default async function (createAppFn, quasarUserOptions) { - - - // Create the app instance. - // Here we inject into it the Quasar UI, the router & possibly the store. - const app = createAppFn(RootComponent) - - - - app.use(Quasar, quasarUserOptions) - - - - - const store = typeof createStore === 'function' - ? await createStore({}) - : createStore - - - app.use(store) - - - - - - const router = markRaw( - typeof createRouter === 'function' - ? await createRouter({store}) - : createRouter - ) - - - // make router instance available in store - - store.use(({ store }) => { store.router = router }) - - - - // Expose the app, the router and the store. - // Note that we are not mounting the app here, since bootstrapping will be - // different depending on whether we are in a browser or on the server. - return { - app, - store, - router - } -} diff --git a/ui/.quasar/prod-spa/client-entry.js b/ui/.quasar/prod-spa/client-entry.js deleted file mode 100644 index 5223e2b..0000000 --- a/ui/.quasar/prod-spa/client-entry.js +++ /dev/null @@ -1,158 +0,0 @@ -/* eslint-disable */ -/** - * THIS FILE IS GENERATED AUTOMATICALLY. - * DO NOT EDIT. - * - * You are probably looking on adding startup/initialization code. - * Use "quasar new boot " and add it there. - * One boot file per concern. Then reference the file(s) in quasar.config file > boot: - * boot: ['file', ...] // do not add ".js" extension to it. - * - * Boot files are your "main.js" - **/ - - -import { createApp } from 'vue' - - - - - - - -import '@quasar/extras/roboto-font/roboto-font.css' - -import '@quasar/extras/material-icons/material-icons.css' - - - - -// We load Quasar stylesheet file -import 'quasar/dist/quasar.sass' - - - - -import 'src/css/app.css' - - -import createQuasarApp from './app.js' -import quasarUserOptions from './quasar-user-options.js' - - - - - - - - -const publicPath = `/` - - -async function start ({ - app, - router - , store -}, bootFiles) { - - let hasRedirected = false - const getRedirectUrl = url => { - try { return router.resolve(url).href } - catch (err) {} - - return Object(url) === url - ? null - : url - } - const redirect = url => { - hasRedirected = true - - if (typeof url === 'string' && /^https?:\/\//.test(url)) { - window.location.href = url - return - } - - const href = getRedirectUrl(url) - - // continue if we didn't fail to resolve the url - if (href !== null) { - window.location.href = href - window.location.reload() - } - } - - const urlPath = window.location.href.replace(window.location.origin, '') - - for (let i = 0; hasRedirected === false && i < bootFiles.length; i++) { - try { - await bootFiles[i]({ - app, - router, - store, - ssrContext: null, - redirect, - urlPath, - publicPath - }) - } - catch (err) { - if (err && err.url) { - redirect(err.url) - return - } - - console.error('[Quasar] boot error:', err) - return - } - } - - if (hasRedirected === true) return - - - app.use(router) - - - - - - - app.mount('#q-app') - - - -} - -createQuasarApp(createApp, quasarUserOptions) - - .then(app => { - // eventually remove this when Cordova/Capacitor/Electron support becomes old - const [ method, mapFn ] = Promise.allSettled !== void 0 - ? [ - 'allSettled', - bootFiles => bootFiles.map(result => { - if (result.status === 'rejected') { - console.error('[Quasar] boot error:', result.reason) - return - } - return result.value.default - }) - ] - : [ - 'all', - bootFiles => bootFiles.map(entry => entry.default) - ] - - return Promise[ method ]([ - - import(/* webpackMode: "eager" */ 'boot/dayjs'), - - import(/* webpackMode: "eager" */ 'boot/locale'), - - import(/* webpackMode: "eager" */ 'boot/resizeObserverGuard') - - ]).then(bootFiles => { - const boot = mapFn(bootFiles).filter(entry => typeof entry === 'function') - start(app, boot) - }) - }) - diff --git a/ui/.quasar/prod-spa/client-prefetch.js b/ui/.quasar/prod-spa/client-prefetch.js deleted file mode 100644 index 9bbe3c5..0000000 --- a/ui/.quasar/prod-spa/client-prefetch.js +++ /dev/null @@ -1,116 +0,0 @@ -/* eslint-disable */ -/** - * THIS FILE IS GENERATED AUTOMATICALLY. - * DO NOT EDIT. - * - * You are probably looking on adding startup/initialization code. - * Use "quasar new boot " and add it there. - * One boot file per concern. Then reference the file(s) in quasar.config file > boot: - * boot: ['file', ...] // do not add ".js" extension to it. - * - * Boot files are your "main.js" - **/ - - - -import App from 'app/src/App.vue' -let appPrefetch = typeof App.preFetch === 'function' - ? App.preFetch - : ( - // Class components return the component options (and the preFetch hook) inside __c property - App.__c !== void 0 && typeof App.__c.preFetch === 'function' - ? App.__c.preFetch - : false - ) - - -function getMatchedComponents (to, router) { - const route = to - ? (to.matched ? to : router.resolve(to).route) - : router.currentRoute.value - - if (!route) { return [] } - - const matched = route.matched.filter(m => m.components !== void 0) - - if (matched.length === 0) { return [] } - - return Array.prototype.concat.apply([], matched.map(m => { - return Object.keys(m.components).map(key => { - const comp = m.components[key] - return { - path: m.path, - c: comp - } - }) - })) -} - -export function addPreFetchHooks ({ router, store, publicPath }) { - // Add router hook for handling preFetch. - // Doing it after initial route is resolved so that we don't double-fetch - // the data that we already have. Using router.beforeResolve() so that all - // async components are resolved. - router.beforeResolve((to, from, next) => { - const - urlPath = window.location.href.replace(window.location.origin, ''), - matched = getMatchedComponents(to, router), - prevMatched = getMatchedComponents(from, router) - - let diffed = false - const preFetchList = matched - .filter((m, i) => { - return diffed || (diffed = ( - !prevMatched[i] || - prevMatched[i].c !== m.c || - m.path.indexOf('/:') > -1 // does it has params? - )) - }) - .filter(m => m.c !== void 0 && ( - typeof m.c.preFetch === 'function' - // Class components return the component options (and the preFetch hook) inside __c property - || (m.c.__c !== void 0 && typeof m.c.__c.preFetch === 'function') - )) - .map(m => m.c.__c !== void 0 ? m.c.__c.preFetch : m.c.preFetch) - - - if (appPrefetch !== false) { - preFetchList.unshift(appPrefetch) - appPrefetch = false - } - - - if (preFetchList.length === 0) { - return next() - } - - let hasRedirected = false - const redirect = url => { - hasRedirected = true - next(url) - } - const proceed = () => { - - if (hasRedirected === false) { next() } - } - - - - preFetchList.reduce( - (promise, preFetch) => promise.then(() => hasRedirected === false && preFetch({ - store, - currentRoute: to, - previousRoute: from, - redirect, - urlPath, - publicPath - })), - Promise.resolve() - ) - .then(proceed) - .catch(e => { - console.error(e) - proceed() - }) - }) -} diff --git a/ui/.quasar/prod-spa/quasar-user-options.js b/ui/.quasar/prod-spa/quasar-user-options.js deleted file mode 100644 index ac1dae3..0000000 --- a/ui/.quasar/prod-spa/quasar-user-options.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -/** - * THIS FILE IS GENERATED AUTOMATICALLY. - * DO NOT EDIT. - * - * You are probably looking on adding startup/initialization code. - * Use "quasar new boot " and add it there. - * One boot file per concern. Then reference the file(s) in quasar.config file > boot: - * boot: ['file', ...] // do not add ".js" extension to it. - * - * Boot files are your "main.js" - **/ - -import lang from 'quasar/lang/tr.js' - - - -import {Loading,Dialog,Notify} from 'quasar' - - - -export default { config: {"notify":{"position":"top","timeout":2500}},lang,plugins: {Loading,Dialog,Notify} } - diff --git a/ui/quasar.config.js.temporary.compiled.1779296132484.mjs b/ui/quasar.config.js.temporary.compiled.1779349901589.mjs similarity index 100% rename from ui/quasar.config.js.temporary.compiled.1779296132484.mjs rename to ui/quasar.config.js.temporary.compiled.1779349901589.mjs diff --git a/ui/src/layouts/MainLayout.vue b/ui/src/layouts/MainLayout.vue index 145ae99..c5d37a7 100644 --- a/ui/src/layouts/MainLayout.vue +++ b/ui/src/layouts/MainLayout.vue @@ -375,6 +375,16 @@ const menuItems = [ label: 'Piyasa Mail Eşleştirme', to: '/app/market-mail-mapping', permission: 'system:update' + }, + { + label: 'Maliyet Mail Eşleştirme', + to: '/app/costing-mail-mapping', + permission: 'system:update' + }, + { + label: 'Fiyatlandırma Mail Eşleştirme', + to: '/app/pricing-mail-mapping', + permission: 'system:update' } ] diff --git a/ui/src/pages/CostingMailMapping.vue b/ui/src/pages/CostingMailMapping.vue new file mode 100644 index 0000000..922c598 --- /dev/null +++ b/ui/src/pages/CostingMailMapping.vue @@ -0,0 +1,172 @@ + + + + diff --git a/ui/src/pages/PricingMailMapping.vue b/ui/src/pages/PricingMailMapping.vue new file mode 100644 index 0000000..ce8fb7b --- /dev/null +++ b/ui/src/pages/PricingMailMapping.vue @@ -0,0 +1,172 @@ + + + + diff --git a/ui/src/router/routes.js b/ui/src/router/routes.js index 6fd2381..606f1ba 100644 --- a/ui/src/router/routes.js +++ b/ui/src/router/routes.js @@ -239,6 +239,18 @@ const routes = [ component: () => import('../pages/MarketMailMapping.vue'), meta: { permission: 'system:update' } }, + { + path: 'costing-mail-mapping', + name: 'costing-mail-mapping', + component: () => import('../pages/CostingMailMapping.vue'), + meta: { permission: 'system:update' } + }, + { + path: 'pricing-mail-mapping', + name: 'pricing-mail-mapping', + component: () => import('../pages/PricingMailMapping.vue'), + meta: { permission: 'system:update' } + }, { path: 'language/translations', name: 'translation-table', diff --git a/ui/src/stores/costingMailMappingStore.js b/ui/src/stores/costingMailMappingStore.js new file mode 100644 index 0000000..2f67ff1 --- /dev/null +++ b/ui/src/stores/costingMailMappingStore.js @@ -0,0 +1,48 @@ +import { defineStore } from 'pinia' +import api from 'src/services/api' + +export const useCostingMailMappingStore = defineStore('costingMailMapping', { + state: () => ({ + loading: false, + saving: false, + firstGroups: [], + mails: [], + rows: [] + }), + + actions: { + async fetchLookups () { + this.loading = true + try { + const res = await api.get('/system/costing-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/costing-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/costing-mail-mappings/${encodeURIComponent(String(urunIlkGrubu || '').trim())}`, { + mail_ids: Array.isArray(mailIds) ? mailIds : [] + }) + } finally { + this.saving = false + } + } + } +}) + diff --git a/ui/src/stores/pricingMailMappingStore.js b/ui/src/stores/pricingMailMappingStore.js new file mode 100644 index 0000000..341e29c --- /dev/null +++ b/ui/src/stores/pricingMailMappingStore.js @@ -0,0 +1,48 @@ +import { defineStore } from 'pinia' +import api from 'src/services/api' + +export const usePricingMailMappingStore = defineStore('pricingMailMapping', { + state: () => ({ + loading: false, + saving: false, + firstGroups: [], + mails: [], + rows: [] + }), + + actions: { + async fetchLookups () { + this.loading = true + try { + const res = await api.get('/system/pricing-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/pricing-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/pricing-mail-mappings/${encodeURIComponent(String(urunIlkGrubu || '').trim())}`, { + mail_ids: Array.isArray(mailIds) ? mailIds : [] + }) + } finally { + this.saving = false + } + } + } +}) +