diff --git a/svc/main.go b/svc/main.go index 195cc20..3b7f584 100644 --- a/svc/main.go +++ b/svc/main.go @@ -395,6 +395,10 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "language", "update", wrapV3(routes.GetTranslationRowsHandler(pgDB)), ) + r.Handle( + "/api/language/translations/runtime", + wrapAuthOnly(http.HandlerFunc(routes.GetRuntimeTranslationsHandler(pgDB))), + ).Methods("GET", "OPTIONS") bindV3(r, pgDB, "/api/language/translations/{id}", "PUT", "language", "update", diff --git a/svc/routes/product_series.go b/svc/routes/product_series.go index 761d976..935ac32 100644 --- a/svc/routes/product_series.go +++ b/svc/routes/product_series.go @@ -695,13 +695,13 @@ LIMIT 1 row.MappingReady = false switch { case row.MmitemID <= 0: - row.MappingWarning = "B2B'de urun yok (mmitem)" + row.MappingWarning = "B2B'de urun yok" case row.Dim1ID <= 0: - row.MappingWarning = "B2B'de renk bu urunde yok (mmitem_dim/dfgrp.code)" + row.MappingWarning = "B2B'de bu urun icin renk yok" case row.Dim3Code != "" && row.Dim3ID <= 0: - row.MappingWarning = "B2B'de dim3 token eslesmesi yok (mk_dim_token_map: dimval3)" + row.MappingWarning = "B2B'de ikinci renk eslesmesi yok" case !comboOK: - row.MappingWarning = "B2B'de varyant kombosu yok (mmitem_dim)" + row.MappingWarning = "B2B'de bu urun/renk/ikinci renk kombinasyonu yok" default: // Not an orphan; skip. if baseReady && comboOK { @@ -861,7 +861,7 @@ func PostProductSeriesMappingsSaveHandler(pg *sql.DB) http.HandlerFunc { } mmitemID, err := resolveMmitemIDTx(ctx, tx, code) if err != nil || mmitemID <= 0 { - http.Error(w, "PG urun bulunamadi: "+code, http.StatusBadRequest) + http.Error(w, "B2B urun bulunamadi: "+code, http.StatusBadRequest) return } // Authoritative dim1 resolver: only allow saving against a color that exists for this product in mmitem_dim. diff --git a/svc/routes/translations.go b/svc/routes/translations.go index 25c6a57..9c6e50a 100644 --- a/svc/routes/translations.go +++ b/svc/routes/translations.go @@ -134,6 +134,12 @@ type TranslateSelectedPayload struct { Limit int `json:"limit"` } +type RuntimeTranslationsResponse struct { + Lang string `json:"lang"` + ByKey map[string]string `json:"by_key"` + ByText map[string]string `json:"by_text"` +} + type BulkUpdateItem struct { ID int64 `json:"id"` SourceTextTR *string `json:"source_text_tr"` @@ -296,6 +302,82 @@ ORDER BY t_key, lang_code } } +func GetRuntimeTranslationsHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + lang := normalizeRuntimeTranslationLang(firstNonEmpty( + r.URL.Query().Get("lang"), + r.Header.Get("Accept-Language"), + )) + if lang == "" { + lang = "tr" + } + + resp := RuntimeTranslationsResponse{ + Lang: lang, + ByKey: map[string]string{}, + ByText: map[string]string{}, + } + + rows, err := db.Query(` +WITH base AS ( + SELECT DISTINCT ON (t_key) + t_key, + COALESCE(NULLIF(source_text_tr, ''), translated_text, '') AS source_text_tr + FROM mk_translator + WHERE COALESCE(NULLIF(source_text_tr, ''), translated_text, '') <> '' + ORDER BY t_key, CASE WHEN lang_code='tr' THEN 0 ELSE 1 END, updated_at DESC +), +target AS ( + SELECT DISTINCT ON (t_key) + t_key, + COALESCE(translated_text, '') AS translated_text + FROM mk_translator + WHERE lang_code=$1 + ORDER BY t_key, is_manual DESC, updated_at DESC +) +SELECT + base.t_key, + base.source_text_tr, + CASE + WHEN $1 = 'tr' THEN base.source_text_tr + ELSE COALESCE(NULLIF(target.translated_text, ''), base.source_text_tr) + END AS display_text +FROM base +LEFT JOIN target ON target.t_key = base.t_key +`, lang) + if err != nil { + http.Error(w, "runtime translations query error", http.StatusInternalServerError) + return + } + defer rows.Close() + + for rows.Next() { + var key, sourceText, displayText string + if err := rows.Scan(&key, &sourceText, &displayText); err != nil { + http.Error(w, "runtime translations scan error", http.StatusInternalServerError) + return + } + key = strings.TrimSpace(key) + sourceText = normalizeRuntimeTranslationText(sourceText) + displayText = normalizeRuntimeTranslationText(displayText) + if key != "" && displayText != "" { + resp.ByKey[key] = displayText + } + if sourceText != "" && displayText != "" { + resp.ByText[sourceText] = displayText + } + } + if err := rows.Err(); err != nil { + http.Error(w, "runtime translations rows error", http.StatusInternalServerError) + return + } + + _ = json.NewEncoder(w).Encode(resp) + } +} + func UpdateTranslationRowHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") @@ -1667,6 +1749,30 @@ func normalizeTranslationLang(v string) string { return "" } +func normalizeRuntimeTranslationLang(v string) string { + raw := strings.ToLower(strings.TrimSpace(v)) + if raw == "" { + return "" + } + if strings.Contains(raw, ",") { + raw = strings.TrimSpace(strings.Split(raw, ",")[0]) + } + if strings.Contains(raw, ";") { + raw = strings.TrimSpace(strings.Split(raw, ";")[0]) + } + if strings.Contains(raw, "-") { + raw = strings.TrimSpace(strings.Split(raw, "-")[0]) + } + if strings.Contains(raw, "_") { + raw = strings.TrimSpace(strings.Split(raw, "_")[0]) + } + return normalizeTranslationLang(raw) +} + +func normalizeRuntimeTranslationText(v string) string { + return strings.Join(strings.Fields(strings.TrimSpace(v)), " ") +} + func normalizeTranslationStatus(v string) string { status := strings.ToLower(strings.TrimSpace(v)) if _, ok := translationStatusSet[status]; ok { diff --git a/ui/src/boot/locale.js b/ui/src/boot/locale.js index e2ee4a2..d8ffb55 100644 --- a/ui/src/boot/locale.js +++ b/ui/src/boot/locale.js @@ -1,7 +1,10 @@ import { boot } from 'quasar/wrappers' import { useLocaleStore } from 'src/stores/localeStore' +import { useRuntimeTranslationStore } from 'src/stores/runtimeTranslationStore' -export default boot(() => { +export default boot(async () => { const localeStore = useLocaleStore() localeStore.setLocale(localeStore.locale) + const runtimeTranslations = useRuntimeTranslationStore() + await runtimeTranslations.loadLocale(localeStore.locale) }) diff --git a/ui/src/composables/useI18n.js b/ui/src/composables/useI18n.js index 7e6988f..c856a4f 100644 --- a/ui/src/composables/useI18n.js +++ b/ui/src/composables/useI18n.js @@ -3,6 +3,7 @@ import { computed } from 'vue' import { messages } from 'src/i18n/messages' import { DEFAULT_LOCALE } from 'src/i18n/languages' import { useLocaleStore } from 'src/stores/localeStore' +import { useRuntimeTranslationStore } from 'src/stores/runtimeTranslationStore' function lookup(obj, path) { return String(path || '') @@ -13,6 +14,7 @@ function lookup(obj, path) { export function useI18n() { const localeStore = useLocaleStore() + const runtimeTranslations = useRuntimeTranslationStore() const currentLocale = computed(() => localeStore.locale) @@ -24,6 +26,11 @@ export function useI18n() { } function t(key) { + const runtimeValue = runtimeTranslations.translateKey(key) + if (runtimeValue && runtimeValue !== key) { + return runtimeValue + } + for (const locale of fallbackLocales(currentLocale.value)) { const val = lookup(messages[locale] || {}, key) if (val != null) return val diff --git a/ui/src/composables/useRuntimeDomTranslations.js b/ui/src/composables/useRuntimeDomTranslations.js new file mode 100644 index 0000000..1dda758 --- /dev/null +++ b/ui/src/composables/useRuntimeDomTranslations.js @@ -0,0 +1,157 @@ +import { nextTick, onBeforeUnmount, onMounted, watch } from 'vue' +import { useRoute } from 'vue-router' + +import { DEFAULT_LOCALE } from 'src/i18n/languages' +import { useLocaleStore } from 'src/stores/localeStore' +import { useRuntimeTranslationStore } from 'src/stores/runtimeTranslationStore' + +const textOriginals = new WeakMap() +const attrOriginals = new WeakMap() +const TRANSLATABLE_ATTRS = ['placeholder', 'title', 'aria-label'] +const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA']) + +function normalizeRuntimeText (value) { + return String(value || '').trim().replace(/\s+/g, ' ') +} + +function preserveOuterWhitespace (original, translated) { + const text = String(original || '') + const leading = text.match(/^\s*/)?.[0] || '' + const trailing = text.match(/\s*$/)?.[0] || '' + return `${leading}${translated}${trailing}` +} + +function shouldSkipNode (node) { + const parent = node?.parentElement + if (!parent) return true + if (SKIP_TAGS.has(parent.tagName)) return true + if (parent.closest?.('[data-no-runtime-i18n="true"]')) return true + return false +} + +export function useRuntimeDomTranslations () { + const route = useRoute() + const localeStore = useLocaleStore() + const runtimeTranslations = useRuntimeTranslationStore() + let observer = null + let applyTimer = null + + function translateTextNode (node) { + if (shouldSkipNode(node)) return + const current = String(node.nodeValue || '') + if (!normalizeRuntimeText(current)) return + + if (!textOriginals.has(node)) { + textOriginals.set(node, current) + } + const original = textOriginals.get(node) + const key = normalizeRuntimeText(original) + const translated = localeStore.locale === DEFAULT_LOCALE + ? key + : runtimeTranslations.byText[key] + + const nextValue = preserveOuterWhitespace(original, translated || key) + if (node.nodeValue !== nextValue) { + node.nodeValue = nextValue + } + } + + function translateElementAttrs (el) { + if (!el || el.nodeType !== Node.ELEMENT_NODE) return + if (el.closest?.('[data-no-runtime-i18n="true"]')) return + let originals = attrOriginals.get(el) + if (!originals) { + originals = {} + attrOriginals.set(el, originals) + } + + for (const attr of TRANSLATABLE_ATTRS) { + if (!el.hasAttribute(attr)) continue + const current = el.getAttribute(attr) + if (!normalizeRuntimeText(current)) continue + if (!Object.prototype.hasOwnProperty.call(originals, attr)) { + originals[attr] = current + } + const key = normalizeRuntimeText(originals[attr]) + const translated = localeStore.locale === DEFAULT_LOCALE + ? key + : runtimeTranslations.byText[key] + const nextValue = translated || key + if (el.getAttribute(attr) !== nextValue) { + el.setAttribute(attr, nextValue) + } + } + } + + function translateRoot (root = document.body) { + if (typeof document === 'undefined' || !root) return + + const walker = document.createTreeWalker( + root, + NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, + { + acceptNode (node) { + if (node.nodeType === Node.TEXT_NODE) { + return normalizeRuntimeText(node.nodeValue) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT + } + return NodeFilter.FILTER_ACCEPT + } + } + ) + + let node = walker.currentNode + while (node) { + if (node.nodeType === Node.TEXT_NODE) { + translateTextNode(node) + } else if (node.nodeType === Node.ELEMENT_NODE) { + translateElementAttrs(node) + } + node = walker.nextNode() + } + } + + function scheduleApply () { + if (applyTimer) clearTimeout(applyTimer) + applyTimer = setTimeout(async () => { + applyTimer = null + await nextTick() + translateRoot(document.body) + }, 30) + } + + onMounted(async () => { + await runtimeTranslations.loadLocale(localeStore.locale) + scheduleApply() + observer = new MutationObserver(() => scheduleApply()) + observer.observe(document.body, { + childList: true, + subtree: true, + characterData: true, + attributes: true, + attributeFilter: TRANSLATABLE_ATTRS + }) + }) + + watch( + () => localeStore.locale, + async (locale) => { + await runtimeTranslations.loadLocale(locale) + scheduleApply() + } + ) + + watch( + () => route.fullPath, + () => scheduleApply() + ) + + watch( + () => runtimeTranslations.version, + () => scheduleApply() + ) + + onBeforeUnmount(() => { + if (observer) observer.disconnect() + if (applyTimer) clearTimeout(applyTimer) + }) +} diff --git a/ui/src/layouts/MainLayout.vue b/ui/src/layouts/MainLayout.vue index 4b8c756..530770f 100644 --- a/ui/src/layouts/MainLayout.vue +++ b/ui/src/layouts/MainLayout.vue @@ -171,6 +171,7 @@ import { Dialog, useQuasar } from 'quasar' import { useAuthStore } from 'stores/authStore' import { usePermissionStore } from 'stores/permissionStore' import { useI18n } from 'src/composables/useI18n' +import { useRuntimeDomTranslations } from 'src/composables/useRuntimeDomTranslations' import { UI_LANGUAGE_OPTIONS } from 'src/i18n/languages' import { useLocaleStore } from 'src/stores/localeStore' import { activityLogsMenuItem, getAuthUserId } from 'src/modules/activityLogs' @@ -185,6 +186,7 @@ const auth = useAuthStore() const perm = usePermissionStore() const localeStore = useLocaleStore() const { t } = useI18n() +useRuntimeDomTranslations() const languageOptions = UI_LANGUAGE_OPTIONS const selectedLocale = computed({ diff --git a/ui/src/pages/BrandClassification.vue b/ui/src/pages/BrandClassification.vue index 1cc9578..81a48bf 100644 --- a/ui/src/pages/BrandClassification.vue +++ b/ui/src/pages/BrandClassification.vue @@ -4,7 +4,7 @@
Marka Sınıflandırma
- Kaynak: BAGGI_V3 `cdItemAttribute` (ItemTypeCode=1, AttributeTypeCode=10) + Kaynak: NEBIM_V3 marka bilgileri
diff --git a/ui/src/pages/PricingRules.vue b/ui/src/pages/PricingRules.vue index 10c40c5..42f478d 100644 --- a/ui/src/pages/PricingRules.vue +++ b/ui/src/pages/PricingRules.vue @@ -371,7 +371,7 @@ const importKeyFieldLabels = [ const importFieldMap = { AKTIF: 'is_active', 'HESAP AKTIF': 'calc_enabled', - 'PG YAYIN': 'publish_postgres', + 'B2B YAYIN': 'publish_postgres', 'NEBIM YAYIN': 'publish_nebim', 'TRY TOPTAN YUVARLAMA': 'try_wholesale_step', 'TRY PERAKENDE MODU': 'try_retail_mode', @@ -454,7 +454,7 @@ const columns = [ col('brand_group', 'MARKA GRUBU', 'brand_group', 76, { classes: 'ps-col', headerClasses: 'ps-col' }), col('anchor_mode', 'ANCHOR MODE', 'anchor_mode', 66, { classes: 'ps-col', headerClasses: 'ps-col' }), col('calc_enabled', 'HESAP AKTIF', 'calc_enabled', 66, { classes: 'ps-col', headerClasses: 'ps-col' }), - col('publish_postgres', 'PG YAYIN', 'publish_postgres', 62, { classes: 'ps-col', headerClasses: 'ps-col' }), + col('publish_postgres', 'B2B YAYIN', 'publish_postgres', 62, { classes: 'ps-col', headerClasses: 'ps-col' }), col('publish_nebim', 'NEBIM YAYIN', 'publish_nebim', 66, { classes: 'ps-col', headerClasses: 'ps-col' }), col('try_wholesale_step', 'TRY TOPTAN YUVARLAMA', 'try_wholesale_step', 76, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }), diff --git a/ui/src/pages/ProductSeriesMappings.vue b/ui/src/pages/ProductSeriesMappings.vue index 8dc8a0b..ac4e8fc 100644 --- a/ui/src/pages/ProductSeriesMappings.vue +++ b/ui/src/pages/ProductSeriesMappings.vue @@ -71,6 +71,31 @@ +
+
+ KATEGORI + + {{ activeFilterCount('kategori') }} + +
+ +
+ + +
+ + +
+
+
+
+
RENK @@ -214,6 +239,7 @@ >
{{ row.product_code || '-' }}
{{ row.product_description || '-' }}
+
{{ row.kategori || '-' }}
{{ variantCode(row) }}
{{ row.color_title || '-' }}
@@ -287,6 +313,7 @@ const errorMessage = ref('') const columnFilters = ref({ model: [], desc: [], + kategori: [], renk: [], ana: [], alt: [], @@ -295,6 +322,7 @@ const columnFilters = ref({ const filterSearch = ref({ model: '', desc: '', + kategori: '', renk: '', ana: '', alt: '', @@ -341,6 +369,7 @@ const productGroups = computed(() => displayRows.value) const filterOptions = computed(() => ({ model: uniqueOptions(rows.value.map(row => row.product_code)), desc: uniqueOptions(rows.value.map(row => row.product_description)), + kategori: uniqueOptions(rows.value.map(row => row.kategori)), renk: uniqueOptions(rows.value.map(row => variantCode(row))), ana: uniqueOptions(rows.value.map(row => row.urun_ana_grubu)), alt: uniqueOptions(rows.value.map(row => row.urun_alt_grubu)), @@ -386,6 +415,7 @@ function clearFilter (key) { function rowPassesFilters (row) { return filterMatch('model', row.product_code) && filterMatch('desc', row.product_description) && + filterMatch('kategori', row.kategori) && filterMatch('renk', variantCode(row)) && filterMatch('ana', row.urun_ana_grubu) && filterMatch('alt', row.urun_alt_grubu) && @@ -548,7 +578,7 @@ function mapRowSizesToSchema (row) { const rawSizeLabels = sizeEntries.map(([rawSize]) => normalizeBedenLabel(rawSize)) const schemaMap = getSchemaMap() const grpKey = detectRowGroupKey(row, rawSizeLabels) - const schema = grpKey && schemaMap?.[grpKey] ? schemaMap[grpKey] : null + const schema = pickSchemaForRow(schemaMap, grpKey, rawSizeLabels) const schemaLabelMap = normalizedSchemaLabelMap(schema) const mapped = {} @@ -569,6 +599,26 @@ function mapRowSizesToSchema (row) { return { grpKey, schema, mapped } } +function pickSchemaForRow (schemaMap, grpKey, rawSizeLabels) { + const primary = grpKey && schemaMap?.[grpKey] ? schemaMap[grpKey] : null + const rawSet = new Set((rawSizeLabels || []).map(v => normalizeBedenLabel(v)).filter(Boolean)) + if (!rawSet.size) return primary + const primarySet = new Set((primary?.values || []).map(v => normalizeBedenLabel(v))) + if ([...rawSet].some(v => primarySet.has(v))) return primary + + let best = primary + let bestScore = 0 + for (const schema of Object.values(schemaMap || {})) { + const schemaSet = new Set((schema?.values || []).map(v => normalizeBedenLabel(v))) + const score = [...rawSet].reduce((sum, v) => sum + (schemaSet.has(v) ? 1 : 0), 0) + if (score > bestScore) { + best = schema + bestScore = score + } + } + return best +} + function formatQty (value) { const n = Number(value || 0) if (!Number.isFinite(n) || n === 0) return '' @@ -605,7 +655,7 @@ function escapeHtml (value) { function exportVisibleExcel () { const groups = schemaRows.value const rowSpan = Math.max(groups.length, 1) - const leftHeaders = ['MODEL', 'DESC', 'RENK', 'URUN ANA GRUBU', 'URUN ALT GRUBU', 'MARKA'] + const leftHeaders = ['MODEL', 'DESC', 'KATEGORI', 'RENK', 'URUN ANA GRUBU', 'URUN ALT GRUBU', 'MARKA'] const headerRows = (groups.length ? groups : [{ key: 'tak', title: 'TAKIM ELBISE', values: [] }]).map((grp, index) => { const left = index === 0 ? leftHeaders.map(h => `${escapeHtml(h)}`).join('') @@ -631,6 +681,7 @@ function exportVisibleExcel () { return ` ${escapeHtml(row.product_code || '')} ${escapeHtml(row.product_description || '')} + ${escapeHtml(row.kategori || '')} ${escapeHtml(variantCode(row))} ${escapeHtml(row.urun_ana_grubu || '')} ${escapeHtml(row.urun_alt_grubu || '')} @@ -665,6 +716,7 @@ onMounted(async () => { --psq-sticky-offset: 12px; --grp-title-w: 90px; --col-desc: var(--col-aciklama); + --col-kategori-series: 86px; --col-marka-series: var(--col-marka, 90px); --psq-header-h: var(--grid-header-h); --series-total-w: 76px; @@ -681,6 +733,7 @@ onMounted(async () => { grid-template-columns: var(--col-model) var(--col-desc) + var(--col-kategori-series) var(--col-renk) var(--col-ana) var(--col-alt) @@ -774,11 +827,11 @@ onMounted(async () => { } .total-header-cell { - grid-column: 8 / 9; + grid-column: 9 / 10; } .series-header-cell { - grid-column: 9 / 10; + grid-column: 10 / 11; } .series-flat-row { @@ -816,12 +869,14 @@ onMounted(async () => { .series-flat-row .sub-col.model { grid-column: 1; } .series-flat-row .sub-col.desc { grid-column: 2; } -.series-flat-row .sub-col.renk { grid-column: 3; } -.series-flat-row .sub-col.ana { grid-column: 4; } -.series-flat-row .sub-col.alt { grid-column: 5; } -.series-flat-row .sub-col.marka { grid-column: 6; } +.series-flat-row .sub-col.kategori { grid-column: 3; } +.series-flat-row .sub-col.renk { grid-column: 4; } +.series-flat-row .sub-col.ana { grid-column: 5; } +.series-flat-row .sub-col.alt { grid-column: 6; } +.series-flat-row .sub-col.marka { grid-column: 7; } .series-flat-row .sub-col.model, +.series-flat-row .sub-col.kategori, .series-flat-row .sub-col.renk, .series-flat-row .sub-col.ana, .series-flat-row .sub-col.alt, @@ -851,7 +906,7 @@ onMounted(async () => { } .flat-size-cells { - grid-column: 7; + grid-column: 8; display: grid; grid-auto-flow: column; grid-auto-columns: var(--beden-w); @@ -893,7 +948,7 @@ onMounted(async () => { } .total-cell { - grid-column: 8; + grid-column: 9; display: flex; align-items: center; justify-content: flex-end; @@ -906,7 +961,7 @@ onMounted(async () => { } .series-select-cell { - grid-column: 9 / 10; + grid-column: 10 / 11; display: flex; flex-direction: column; justify-content: center; diff --git a/ui/src/pages/ProductSeriesMappingsOrphans.vue b/ui/src/pages/ProductSeriesMappingsOrphans.vue index f62a659..3400539 100644 --- a/ui/src/pages/ProductSeriesMappingsOrphans.vue +++ b/ui/src/pages/ProductSeriesMappingsOrphans.vue @@ -70,6 +70,31 @@
+
+
+ KATEGORI + + {{ activeFilterCount('kategori') }} + +
+ +
+ + +
+ + +
+
+
+
+
RENK @@ -212,6 +237,7 @@ >
{{ row.product_code || '-' }}
{{ row.product_description || '-' }}
+
{{ row.kategori || '-' }}
{{ variantCode(row) }}
{{ row.color_title || '-' }}
@@ -273,6 +299,7 @@ const errorMessage = ref('') const columnFilters = ref({ model: [], desc: [], + kategori: [], renk: [], ana: [], alt: [], @@ -281,6 +308,7 @@ const columnFilters = ref({ const filterSearch = ref({ model: '', desc: '', + kategori: '', renk: '', ana: '', alt: '', @@ -325,6 +353,7 @@ const productGroups = computed(() => displayRows.value) const filterOptions = computed(() => ({ model: uniqueOptions(rows.value.map(row => row.product_code)), desc: uniqueOptions(rows.value.map(row => row.product_description)), + kategori: uniqueOptions(rows.value.map(row => row.kategori)), renk: uniqueOptions(rows.value.map(row => variantCode(row))), ana: uniqueOptions(rows.value.map(row => row.urun_ana_grubu)), alt: uniqueOptions(rows.value.map(row => row.urun_alt_grubu)), @@ -370,6 +399,7 @@ function clearFilter (key) { function rowPassesFilters (row) { return filterMatch('model', row.product_code) && filterMatch('desc', row.product_description) && + filterMatch('kategori', row.kategori) && filterMatch('renk', variantCode(row)) && filterMatch('ana', row.urun_ana_grubu) && filterMatch('alt', row.urun_alt_grubu) && @@ -492,7 +522,7 @@ function mapRowSizesToSchema (row) { const rawSizeLabels = sizeEntries.map(([rawSize]) => normalizeBedenLabel(rawSize)) const schemaMap = getSchemaMap() const grpKey = detectRowGroupKey(row, rawSizeLabels) - const schema = grpKey && schemaMap?.[grpKey] ? schemaMap[grpKey] : null + const schema = pickSchemaForRow(schemaMap, grpKey, rawSizeLabels) const schemaLabelMap = normalizedSchemaLabelMap(schema) const mapped = {} @@ -513,6 +543,26 @@ function mapRowSizesToSchema (row) { return { grpKey, schema, mapped } } +function pickSchemaForRow (schemaMap, grpKey, rawSizeLabels) { + const primary = grpKey && schemaMap?.[grpKey] ? schemaMap[grpKey] : null + const rawSet = new Set((rawSizeLabels || []).map(v => normalizeBedenLabel(v)).filter(Boolean)) + if (!rawSet.size) return primary + const primarySet = new Set((primary?.values || []).map(v => normalizeBedenLabel(v))) + if ([...rawSet].some(v => primarySet.has(v))) return primary + + let best = primary + let bestScore = 0 + for (const schema of Object.values(schemaMap || {})) { + const schemaSet = new Set((schema?.values || []).map(v => normalizeBedenLabel(v))) + const score = [...rawSet].reduce((sum, v) => sum + (schemaSet.has(v) ? 1 : 0), 0) + if (score > bestScore) { + best = schema + bestScore = score + } + } + return best +} + function formatQty (value) { const n = Number(value || 0) if (!Number.isFinite(n) || n === 0) return '' @@ -544,7 +594,7 @@ function csvEscape (v) { } function exportVisibleExcel () { - const cols = ['MODEL', 'DESC', 'RENK', 'URUN ANA GRUBU', 'URUN ALT GRUBU', 'MARKA'] + const cols = ['MODEL', 'DESC', 'KATEGORI', 'RENK', 'URUN ANA GRUBU', 'URUN ALT GRUBU', 'MARKA'] const sizeCols = schemaRows.value.flatMap(grp => paddedSchemaValues(grp).map(v => (v.ghost ? '' : v.value))).filter(Boolean) cols.push(...sizeCols) cols.push('TOPLAM', 'SERI', 'UYARI') @@ -559,6 +609,7 @@ function exportVisibleExcel () { lines.push([ row.product_code || '', row.product_description || '', + row.kategori || '', variantCode(row), row.urun_ana_grubu || '', row.urun_alt_grubu || '', @@ -590,6 +641,7 @@ onMounted(() => reload()) --psq-sticky-offset: 12px; --grp-title-w: 90px; --col-desc: var(--col-aciklama); + --col-kategori-series: 86px; --col-marka-series: var(--col-marka, 90px); --psq-header-h: var(--grid-header-h); --series-total-w: 76px; @@ -606,6 +658,7 @@ onMounted(() => reload()) grid-template-columns: var(--col-model) var(--col-desc) + var(--col-kategori-series) var(--col-renk) var(--col-ana) var(--col-alt) @@ -699,11 +752,11 @@ onMounted(() => reload()) } .total-header-cell { - grid-column: 8 / 9; + grid-column: 9 / 10; } .series-header-cell { - grid-column: 9 / 10; + grid-column: 10 / 11; } .series-flat-row { @@ -741,12 +794,14 @@ onMounted(() => reload()) .series-flat-row .sub-col.model { grid-column: 1; } .series-flat-row .sub-col.desc { grid-column: 2; } -.series-flat-row .sub-col.renk { grid-column: 3; } -.series-flat-row .sub-col.ana { grid-column: 4; } -.series-flat-row .sub-col.alt { grid-column: 5; } -.series-flat-row .sub-col.marka { grid-column: 6; } +.series-flat-row .sub-col.kategori { grid-column: 3; } +.series-flat-row .sub-col.renk { grid-column: 4; } +.series-flat-row .sub-col.ana { grid-column: 5; } +.series-flat-row .sub-col.alt { grid-column: 6; } +.series-flat-row .sub-col.marka { grid-column: 7; } .series-flat-row .sub-col.model, +.series-flat-row .sub-col.kategori, .series-flat-row .sub-col.renk, .series-flat-row .sub-col.ana, .series-flat-row .sub-col.alt, @@ -776,7 +831,7 @@ onMounted(() => reload()) } .flat-size-cells { - grid-column: 7; + grid-column: 8; display: grid; grid-auto-flow: column; grid-auto-columns: var(--beden-w); @@ -818,7 +873,7 @@ onMounted(() => reload()) } .total-cell { - grid-column: 8; + grid-column: 9; display: flex; align-items: center; justify-content: flex-end; @@ -831,7 +886,7 @@ onMounted(() => reload()) } .series-select-cell { - grid-column: 9 / 10; + grid-column: 10 / 11; display: flex; flex-direction: column; justify-content: center; diff --git a/ui/src/pages/ProductionProductCostingHasCostDetail.vue b/ui/src/pages/ProductionProductCostingHasCostDetail.vue index 60b4bc3..eccb84d 100644 --- a/ui/src/pages/ProductionProductCostingHasCostDetail.vue +++ b/ui/src/pages/ProductionProductCostingHasCostDetail.vue @@ -840,7 +840,7 @@
Fiyat Kontrolu (Satinalma Ortalama)
- BAGGI_V3 satinalma gecmisindeki son 10 USD ortalamasindan %10'dan fazla sapan tum satirlar. + NEBIM_V3 satinalma gecmisindeki son 10 USD ortalamasindan %10'dan fazla sapan tum satirlar.
{{ priceDeviationRows.length }} satir @@ -1105,6 +1105,7 @@ import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router' import { usePermission } from 'src/composables/usePermission' import { get, post, download, extractApiErrorDetail } from 'src/services/api' import { createTraceId, slog } from 'src/utils/slog' +import { formatDataSourceLabel } from 'src/utils/formatters' const route = useRoute() const router = useRouter() @@ -2334,8 +2335,8 @@ function resolveHistorySourceType (item, forcedType = '') { } function resolveHistorySourceLabel (sourceType) { - if (sourceType === 'purchase') return 'BAGGI_V3' - if (sourceType === 'recipe') return 'URETIM' + if (sourceType === 'purchase') return formatDataSourceLabel('BAGGI_V3') + if (sourceType === 'recipe') return formatDataSourceLabel('URETIM') return 'HISTORY' } @@ -4770,7 +4771,7 @@ async function confirmBrPriceDeviationIfNeeded () { html: true, message: `
- Bazı satırlarda girilen fiyat, BAGGI_V3 satınalma geçmişindeki son 10 kaydın USD ortalamasından %10'dan fazla sapıyor. + Bazı satırlarda girilen fiyat, NEBIM_V3 satınalma geçmişindeki son 10 kaydın USD ortalamasından %10'dan fazla sapıyor.
diff --git a/ui/src/pages/ProductionProductCostingMTBolumMapping.vue b/ui/src/pages/ProductionProductCostingMTBolumMapping.vue index c45e4ec..4cf51a1 100644 --- a/ui/src/pages/ProductionProductCostingMTBolumMapping.vue +++ b/ui/src/pages/ProductionProductCostingMTBolumMapping.vue @@ -5,7 +5,7 @@
Maliyet Parca Eslestirme
- V3 Urun Ilk Grubu (42. ozellik) + Urun Ana/Alt Grup + URETIM Parca Bolum + Hammadde Turleri eslestirmesi (URETIM mk_ tablolarinda tutulur) + NEBIM_V3 urun gruplari + TEDARIK URETIM PROGRAMI parca bolum + hammadde turleri eslestirmesi
diff --git a/ui/src/pages/TranslationTable.vue b/ui/src/pages/TranslationTable.vue index b61215c..7da60bc 100644 --- a/ui/src/pages/TranslationTable.vue +++ b/ui/src/pages/TranslationTable.vue @@ -207,6 +207,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { useQuasar } from 'quasar' import { usePermission } from 'src/composables/usePermission' import { useTranslationStore } from 'src/stores/translationStore' +import { formatDataSourceLabel } from 'src/utils/formatters' const $q = useQuasar() const store = useTranslationStore() @@ -231,15 +232,10 @@ const tablePagination = ref({ let filterReloadTimer = null const sourceTypeOptions = [ - { label: 'dummy', value: 'dummy' }, - { label: 'postgre', value: 'postgre' }, - { label: 'mssql', value: 'mssql' } + { label: formatDataSourceLabel('dummy'), value: 'dummy' }, + { label: formatDataSourceLabel('postgre'), value: 'postgre' }, + { label: formatDataSourceLabel('mssql'), value: 'mssql' } ] -const sourceTypeLabelMap = { - dummy: 'UI', - postgre: 'PostgreSQL', - mssql: 'MSSQL' -} const columns = [ { name: 'actions', label: 'Güncelle', field: 'actions', align: 'left' }, @@ -413,7 +409,7 @@ function cellClass (key, field) { function sourceTypeLabel (key) { const val = String(rowDraft(key).source_type || 'dummy').toLowerCase() - return sourceTypeLabelMap[val] || val || '-' + return formatDataSourceLabel(val) } function toggleSelected (key, checked) { diff --git a/ui/src/pages/UserSync.vue b/ui/src/pages/UserSync.vue index d65fb31..a92498f 100644 --- a/ui/src/pages/UserSync.vue +++ b/ui/src/pages/UserSync.vue @@ -3,9 +3,8 @@ v-if="canReadUser" class="q-pa-md user-sync-page" > -
-
👤 Kullanıcı Yönetimi
+
Kullanici Yonetimi
-
- PostgreSQL Kullanıcıları + B2B Kullanicilari -