ui: add B2B olmayan stok (orphans) page
This commit is contained in:
@@ -395,6 +395,10 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
"language", "update",
|
"language", "update",
|
||||||
wrapV3(routes.GetTranslationRowsHandler(pgDB)),
|
wrapV3(routes.GetTranslationRowsHandler(pgDB)),
|
||||||
)
|
)
|
||||||
|
r.Handle(
|
||||||
|
"/api/language/translations/runtime",
|
||||||
|
wrapAuthOnly(http.HandlerFunc(routes.GetRuntimeTranslationsHandler(pgDB))),
|
||||||
|
).Methods("GET", "OPTIONS")
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/language/translations/{id}", "PUT",
|
"/api/language/translations/{id}", "PUT",
|
||||||
"language", "update",
|
"language", "update",
|
||||||
|
|||||||
@@ -695,13 +695,13 @@ LIMIT 1
|
|||||||
row.MappingReady = false
|
row.MappingReady = false
|
||||||
switch {
|
switch {
|
||||||
case row.MmitemID <= 0:
|
case row.MmitemID <= 0:
|
||||||
row.MappingWarning = "B2B'de urun yok (mmitem)"
|
row.MappingWarning = "B2B'de urun yok"
|
||||||
case row.Dim1ID <= 0:
|
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:
|
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:
|
case !comboOK:
|
||||||
row.MappingWarning = "B2B'de varyant kombosu yok (mmitem_dim)"
|
row.MappingWarning = "B2B'de bu urun/renk/ikinci renk kombinasyonu yok"
|
||||||
default:
|
default:
|
||||||
// Not an orphan; skip.
|
// Not an orphan; skip.
|
||||||
if baseReady && comboOK {
|
if baseReady && comboOK {
|
||||||
@@ -861,7 +861,7 @@ func PostProductSeriesMappingsSaveHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
mmitemID, err := resolveMmitemIDTx(ctx, tx, code)
|
mmitemID, err := resolveMmitemIDTx(ctx, tx, code)
|
||||||
if err != nil || mmitemID <= 0 {
|
if err != nil || mmitemID <= 0 {
|
||||||
http.Error(w, "PG urun bulunamadi: "+code, http.StatusBadRequest)
|
http.Error(w, "B2B urun bulunamadi: "+code, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Authoritative dim1 resolver: only allow saving against a color that exists for this product in mmitem_dim.
|
// Authoritative dim1 resolver: only allow saving against a color that exists for this product in mmitem_dim.
|
||||||
|
|||||||
@@ -134,6 +134,12 @@ type TranslateSelectedPayload struct {
|
|||||||
Limit int `json:"limit"`
|
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 {
|
type BulkUpdateItem struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
SourceTextTR *string `json:"source_text_tr"`
|
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 {
|
func UpdateTranslationRowHandler(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
@@ -1667,6 +1749,30 @@ func normalizeTranslationLang(v string) string {
|
|||||||
return ""
|
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 {
|
func normalizeTranslationStatus(v string) string {
|
||||||
status := strings.ToLower(strings.TrimSpace(v))
|
status := strings.ToLower(strings.TrimSpace(v))
|
||||||
if _, ok := translationStatusSet[status]; ok {
|
if _, ok := translationStatusSet[status]; ok {
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { boot } from 'quasar/wrappers'
|
import { boot } from 'quasar/wrappers'
|
||||||
import { useLocaleStore } from 'src/stores/localeStore'
|
import { useLocaleStore } from 'src/stores/localeStore'
|
||||||
|
import { useRuntimeTranslationStore } from 'src/stores/runtimeTranslationStore'
|
||||||
|
|
||||||
export default boot(() => {
|
export default boot(async () => {
|
||||||
const localeStore = useLocaleStore()
|
const localeStore = useLocaleStore()
|
||||||
localeStore.setLocale(localeStore.locale)
|
localeStore.setLocale(localeStore.locale)
|
||||||
|
const runtimeTranslations = useRuntimeTranslationStore()
|
||||||
|
await runtimeTranslations.loadLocale(localeStore.locale)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { computed } from 'vue'
|
|||||||
import { messages } from 'src/i18n/messages'
|
import { messages } from 'src/i18n/messages'
|
||||||
import { DEFAULT_LOCALE } from 'src/i18n/languages'
|
import { DEFAULT_LOCALE } from 'src/i18n/languages'
|
||||||
import { useLocaleStore } from 'src/stores/localeStore'
|
import { useLocaleStore } from 'src/stores/localeStore'
|
||||||
|
import { useRuntimeTranslationStore } from 'src/stores/runtimeTranslationStore'
|
||||||
|
|
||||||
function lookup(obj, path) {
|
function lookup(obj, path) {
|
||||||
return String(path || '')
|
return String(path || '')
|
||||||
@@ -13,6 +14,7 @@ function lookup(obj, path) {
|
|||||||
|
|
||||||
export function useI18n() {
|
export function useI18n() {
|
||||||
const localeStore = useLocaleStore()
|
const localeStore = useLocaleStore()
|
||||||
|
const runtimeTranslations = useRuntimeTranslationStore()
|
||||||
|
|
||||||
const currentLocale = computed(() => localeStore.locale)
|
const currentLocale = computed(() => localeStore.locale)
|
||||||
|
|
||||||
@@ -24,6 +26,11 @@ export function useI18n() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function t(key) {
|
function t(key) {
|
||||||
|
const runtimeValue = runtimeTranslations.translateKey(key)
|
||||||
|
if (runtimeValue && runtimeValue !== key) {
|
||||||
|
return runtimeValue
|
||||||
|
}
|
||||||
|
|
||||||
for (const locale of fallbackLocales(currentLocale.value)) {
|
for (const locale of fallbackLocales(currentLocale.value)) {
|
||||||
const val = lookup(messages[locale] || {}, key)
|
const val = lookup(messages[locale] || {}, key)
|
||||||
if (val != null) return val
|
if (val != null) return val
|
||||||
|
|||||||
157
ui/src/composables/useRuntimeDomTranslations.js
Normal file
157
ui/src/composables/useRuntimeDomTranslations.js
Normal file
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -171,6 +171,7 @@ import { Dialog, useQuasar } from 'quasar'
|
|||||||
import { useAuthStore } from 'stores/authStore'
|
import { useAuthStore } from 'stores/authStore'
|
||||||
import { usePermissionStore } from 'stores/permissionStore'
|
import { usePermissionStore } from 'stores/permissionStore'
|
||||||
import { useI18n } from 'src/composables/useI18n'
|
import { useI18n } from 'src/composables/useI18n'
|
||||||
|
import { useRuntimeDomTranslations } from 'src/composables/useRuntimeDomTranslations'
|
||||||
import { UI_LANGUAGE_OPTIONS } from 'src/i18n/languages'
|
import { UI_LANGUAGE_OPTIONS } from 'src/i18n/languages'
|
||||||
import { useLocaleStore } from 'src/stores/localeStore'
|
import { useLocaleStore } from 'src/stores/localeStore'
|
||||||
import { activityLogsMenuItem, getAuthUserId } from 'src/modules/activityLogs'
|
import { activityLogsMenuItem, getAuthUserId } from 'src/modules/activityLogs'
|
||||||
@@ -185,6 +186,7 @@ const auth = useAuthStore()
|
|||||||
const perm = usePermissionStore()
|
const perm = usePermissionStore()
|
||||||
const localeStore = useLocaleStore()
|
const localeStore = useLocaleStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
useRuntimeDomTranslations()
|
||||||
|
|
||||||
const languageOptions = UI_LANGUAGE_OPTIONS
|
const languageOptions = UI_LANGUAGE_OPTIONS
|
||||||
const selectedLocale = computed({
|
const selectedLocale = computed({
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="col-12 col-md">
|
<div class="col-12 col-md">
|
||||||
<div class="text-h6">Marka Sınıflandırma</div>
|
<div class="text-h6">Marka Sınıflandırma</div>
|
||||||
<div class="text-caption text-grey-7">
|
<div class="text-caption text-grey-7">
|
||||||
Kaynak: BAGGI_V3 `cdItemAttribute` (ItemTypeCode=1, AttributeTypeCode=10)
|
Kaynak: NEBIM_V3 marka bilgileri
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -371,7 +371,7 @@ const importKeyFieldLabels = [
|
|||||||
const importFieldMap = {
|
const importFieldMap = {
|
||||||
AKTIF: 'is_active',
|
AKTIF: 'is_active',
|
||||||
'HESAP AKTIF': 'calc_enabled',
|
'HESAP AKTIF': 'calc_enabled',
|
||||||
'PG YAYIN': 'publish_postgres',
|
'B2B YAYIN': 'publish_postgres',
|
||||||
'NEBIM YAYIN': 'publish_nebim',
|
'NEBIM YAYIN': 'publish_nebim',
|
||||||
'TRY TOPTAN YUVARLAMA': 'try_wholesale_step',
|
'TRY TOPTAN YUVARLAMA': 'try_wholesale_step',
|
||||||
'TRY PERAKENDE MODU': 'try_retail_mode',
|
'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('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('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('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('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' }),
|
col('try_wholesale_step', 'TRY TOPTAN YUVARLAMA', 'try_wholesale_step', 76, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||||
|
|||||||
@@ -71,6 +71,31 @@
|
|||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-fixed kategori">
|
||||||
|
<div class="filter-head">
|
||||||
|
<span>KATEGORI</span>
|
||||||
|
<q-btn dense flat round size="sm" icon="filter_list" :color="activeFilterCount('kategori') ? 'primary' : 'grey-8'">
|
||||||
|
<q-badge v-if="activeFilterCount('kategori')" floating color="primary">{{ activeFilterCount('kategori') }}</q-badge>
|
||||||
|
<q-menu class="column-filter-menu" anchor="bottom left" self="top left">
|
||||||
|
<div class="q-pa-sm">
|
||||||
|
<q-input v-model="filterSearch.kategori" dense outlined clearable placeholder="Ara" autofocus />
|
||||||
|
<div class="row q-gutter-xs q-mt-sm">
|
||||||
|
<q-btn dense flat size="sm" label="Tumu" @click="selectAllFilter('kategori')" />
|
||||||
|
<q-btn dense flat size="sm" label="Temizle" @click="clearFilter('kategori')" />
|
||||||
|
</div>
|
||||||
|
<q-separator class="q-my-sm" />
|
||||||
|
<q-option-group
|
||||||
|
v-model="columnFilters.kategori"
|
||||||
|
:options="filteredFilterOptions('kategori')"
|
||||||
|
type="checkbox"
|
||||||
|
dense
|
||||||
|
class="column-filter-options"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="col-fixed renk">
|
<div class="col-fixed renk">
|
||||||
<div class="filter-head">
|
<div class="filter-head">
|
||||||
<span>RENK</span>
|
<span>RENK</span>
|
||||||
@@ -214,6 +239,7 @@
|
|||||||
>
|
>
|
||||||
<div class="sub-col model">{{ row.product_code || '-' }}</div>
|
<div class="sub-col model">{{ row.product_code || '-' }}</div>
|
||||||
<div class="sub-col desc">{{ row.product_description || '-' }}</div>
|
<div class="sub-col desc">{{ row.product_description || '-' }}</div>
|
||||||
|
<div class="sub-col kategori">{{ row.kategori || '-' }}</div>
|
||||||
<div class="sub-col renk">
|
<div class="sub-col renk">
|
||||||
<div class="renk-kodu">{{ variantCode(row) }}</div>
|
<div class="renk-kodu">{{ variantCode(row) }}</div>
|
||||||
<div class="renk-aciklama">{{ row.color_title || '-' }}</div>
|
<div class="renk-aciklama">{{ row.color_title || '-' }}</div>
|
||||||
@@ -287,6 +313,7 @@ const errorMessage = ref('')
|
|||||||
const columnFilters = ref({
|
const columnFilters = ref({
|
||||||
model: [],
|
model: [],
|
||||||
desc: [],
|
desc: [],
|
||||||
|
kategori: [],
|
||||||
renk: [],
|
renk: [],
|
||||||
ana: [],
|
ana: [],
|
||||||
alt: [],
|
alt: [],
|
||||||
@@ -295,6 +322,7 @@ const columnFilters = ref({
|
|||||||
const filterSearch = ref({
|
const filterSearch = ref({
|
||||||
model: '',
|
model: '',
|
||||||
desc: '',
|
desc: '',
|
||||||
|
kategori: '',
|
||||||
renk: '',
|
renk: '',
|
||||||
ana: '',
|
ana: '',
|
||||||
alt: '',
|
alt: '',
|
||||||
@@ -341,6 +369,7 @@ const productGroups = computed(() => displayRows.value)
|
|||||||
const filterOptions = computed(() => ({
|
const filterOptions = computed(() => ({
|
||||||
model: uniqueOptions(rows.value.map(row => row.product_code)),
|
model: uniqueOptions(rows.value.map(row => row.product_code)),
|
||||||
desc: uniqueOptions(rows.value.map(row => row.product_description)),
|
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))),
|
renk: uniqueOptions(rows.value.map(row => variantCode(row))),
|
||||||
ana: uniqueOptions(rows.value.map(row => row.urun_ana_grubu)),
|
ana: uniqueOptions(rows.value.map(row => row.urun_ana_grubu)),
|
||||||
alt: uniqueOptions(rows.value.map(row => row.urun_alt_grubu)),
|
alt: uniqueOptions(rows.value.map(row => row.urun_alt_grubu)),
|
||||||
@@ -386,6 +415,7 @@ function clearFilter (key) {
|
|||||||
function rowPassesFilters (row) {
|
function rowPassesFilters (row) {
|
||||||
return filterMatch('model', row.product_code) &&
|
return filterMatch('model', row.product_code) &&
|
||||||
filterMatch('desc', row.product_description) &&
|
filterMatch('desc', row.product_description) &&
|
||||||
|
filterMatch('kategori', row.kategori) &&
|
||||||
filterMatch('renk', variantCode(row)) &&
|
filterMatch('renk', variantCode(row)) &&
|
||||||
filterMatch('ana', row.urun_ana_grubu) &&
|
filterMatch('ana', row.urun_ana_grubu) &&
|
||||||
filterMatch('alt', row.urun_alt_grubu) &&
|
filterMatch('alt', row.urun_alt_grubu) &&
|
||||||
@@ -548,7 +578,7 @@ function mapRowSizesToSchema (row) {
|
|||||||
const rawSizeLabels = sizeEntries.map(([rawSize]) => normalizeBedenLabel(rawSize))
|
const rawSizeLabels = sizeEntries.map(([rawSize]) => normalizeBedenLabel(rawSize))
|
||||||
const schemaMap = getSchemaMap()
|
const schemaMap = getSchemaMap()
|
||||||
const grpKey = detectRowGroupKey(row, rawSizeLabels)
|
const grpKey = detectRowGroupKey(row, rawSizeLabels)
|
||||||
const schema = grpKey && schemaMap?.[grpKey] ? schemaMap[grpKey] : null
|
const schema = pickSchemaForRow(schemaMap, grpKey, rawSizeLabels)
|
||||||
const schemaLabelMap = normalizedSchemaLabelMap(schema)
|
const schemaLabelMap = normalizedSchemaLabelMap(schema)
|
||||||
const mapped = {}
|
const mapped = {}
|
||||||
|
|
||||||
@@ -569,6 +599,26 @@ function mapRowSizesToSchema (row) {
|
|||||||
return { grpKey, schema, mapped }
|
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) {
|
function formatQty (value) {
|
||||||
const n = Number(value || 0)
|
const n = Number(value || 0)
|
||||||
if (!Number.isFinite(n) || n === 0) return ''
|
if (!Number.isFinite(n) || n === 0) return ''
|
||||||
@@ -605,7 +655,7 @@ function escapeHtml (value) {
|
|||||||
function exportVisibleExcel () {
|
function exportVisibleExcel () {
|
||||||
const groups = schemaRows.value
|
const groups = schemaRows.value
|
||||||
const rowSpan = Math.max(groups.length, 1)
|
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 headerRows = (groups.length ? groups : [{ key: 'tak', title: 'TAKIM ELBISE', values: [] }]).map((grp, index) => {
|
||||||
const left = index === 0
|
const left = index === 0
|
||||||
? leftHeaders.map(h => `<th rowspan="${rowSpan}">${escapeHtml(h)}</th>`).join('')
|
? leftHeaders.map(h => `<th rowspan="${rowSpan}">${escapeHtml(h)}</th>`).join('')
|
||||||
@@ -631,6 +681,7 @@ function exportVisibleExcel () {
|
|||||||
return `<tr>
|
return `<tr>
|
||||||
<td>${escapeHtml(row.product_code || '')}</td>
|
<td>${escapeHtml(row.product_code || '')}</td>
|
||||||
<td>${escapeHtml(row.product_description || '')}</td>
|
<td>${escapeHtml(row.product_description || '')}</td>
|
||||||
|
<td>${escapeHtml(row.kategori || '')}</td>
|
||||||
<td>${escapeHtml(variantCode(row))}</td>
|
<td>${escapeHtml(variantCode(row))}</td>
|
||||||
<td>${escapeHtml(row.urun_ana_grubu || '')}</td>
|
<td>${escapeHtml(row.urun_ana_grubu || '')}</td>
|
||||||
<td>${escapeHtml(row.urun_alt_grubu || '')}</td>
|
<td>${escapeHtml(row.urun_alt_grubu || '')}</td>
|
||||||
@@ -665,6 +716,7 @@ onMounted(async () => {
|
|||||||
--psq-sticky-offset: 12px;
|
--psq-sticky-offset: 12px;
|
||||||
--grp-title-w: 90px;
|
--grp-title-w: 90px;
|
||||||
--col-desc: var(--col-aciklama);
|
--col-desc: var(--col-aciklama);
|
||||||
|
--col-kategori-series: 86px;
|
||||||
--col-marka-series: var(--col-marka, 90px);
|
--col-marka-series: var(--col-marka, 90px);
|
||||||
--psq-header-h: var(--grid-header-h);
|
--psq-header-h: var(--grid-header-h);
|
||||||
--series-total-w: 76px;
|
--series-total-w: 76px;
|
||||||
@@ -681,6 +733,7 @@ onMounted(async () => {
|
|||||||
grid-template-columns:
|
grid-template-columns:
|
||||||
var(--col-model)
|
var(--col-model)
|
||||||
var(--col-desc)
|
var(--col-desc)
|
||||||
|
var(--col-kategori-series)
|
||||||
var(--col-renk)
|
var(--col-renk)
|
||||||
var(--col-ana)
|
var(--col-ana)
|
||||||
var(--col-alt)
|
var(--col-alt)
|
||||||
@@ -774,11 +827,11 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.total-header-cell {
|
.total-header-cell {
|
||||||
grid-column: 8 / 9;
|
grid-column: 9 / 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-header-cell {
|
.series-header-cell {
|
||||||
grid-column: 9 / 10;
|
grid-column: 10 / 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-flat-row {
|
.series-flat-row {
|
||||||
@@ -816,12 +869,14 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.series-flat-row .sub-col.model { grid-column: 1; }
|
.series-flat-row .sub-col.model { grid-column: 1; }
|
||||||
.series-flat-row .sub-col.desc { grid-column: 2; }
|
.series-flat-row .sub-col.desc { grid-column: 2; }
|
||||||
.series-flat-row .sub-col.renk { grid-column: 3; }
|
.series-flat-row .sub-col.kategori { grid-column: 3; }
|
||||||
.series-flat-row .sub-col.ana { grid-column: 4; }
|
.series-flat-row .sub-col.renk { grid-column: 4; }
|
||||||
.series-flat-row .sub-col.alt { grid-column: 5; }
|
.series-flat-row .sub-col.ana { grid-column: 5; }
|
||||||
.series-flat-row .sub-col.marka { grid-column: 6; }
|
.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.model,
|
||||||
|
.series-flat-row .sub-col.kategori,
|
||||||
.series-flat-row .sub-col.renk,
|
.series-flat-row .sub-col.renk,
|
||||||
.series-flat-row .sub-col.ana,
|
.series-flat-row .sub-col.ana,
|
||||||
.series-flat-row .sub-col.alt,
|
.series-flat-row .sub-col.alt,
|
||||||
@@ -851,7 +906,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.flat-size-cells {
|
.flat-size-cells {
|
||||||
grid-column: 7;
|
grid-column: 8;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
grid-auto-columns: var(--beden-w);
|
grid-auto-columns: var(--beden-w);
|
||||||
@@ -893,7 +948,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.total-cell {
|
.total-cell {
|
||||||
grid-column: 8;
|
grid-column: 9;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -906,7 +961,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.series-select-cell {
|
.series-select-cell {
|
||||||
grid-column: 9 / 10;
|
grid-column: 10 / 11;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -70,6 +70,31 @@
|
|||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-fixed kategori">
|
||||||
|
<div class="filter-head">
|
||||||
|
<span>KATEGORI</span>
|
||||||
|
<q-btn dense flat round size="sm" icon="filter_list" :color="activeFilterCount('kategori') ? 'primary' : 'grey-8'">
|
||||||
|
<q-badge v-if="activeFilterCount('kategori')" floating color="primary">{{ activeFilterCount('kategori') }}</q-badge>
|
||||||
|
<q-menu class="column-filter-menu" anchor="bottom left" self="top left">
|
||||||
|
<div class="q-pa-sm">
|
||||||
|
<q-input v-model="filterSearch.kategori" dense outlined clearable placeholder="Ara" autofocus />
|
||||||
|
<div class="row q-gutter-xs q-mt-sm">
|
||||||
|
<q-btn dense flat size="sm" label="Tumu" @click="selectAllFilter('kategori')" />
|
||||||
|
<q-btn dense flat size="sm" label="Temizle" @click="clearFilter('kategori')" />
|
||||||
|
</div>
|
||||||
|
<q-separator class="q-my-sm" />
|
||||||
|
<q-option-group
|
||||||
|
v-model="columnFilters.kategori"
|
||||||
|
:options="filteredFilterOptions('kategori')"
|
||||||
|
type="checkbox"
|
||||||
|
dense
|
||||||
|
class="column-filter-options"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="col-fixed renk">
|
<div class="col-fixed renk">
|
||||||
<div class="filter-head">
|
<div class="filter-head">
|
||||||
<span>RENK</span>
|
<span>RENK</span>
|
||||||
@@ -212,6 +237,7 @@
|
|||||||
>
|
>
|
||||||
<div class="sub-col model">{{ row.product_code || '-' }}</div>
|
<div class="sub-col model">{{ row.product_code || '-' }}</div>
|
||||||
<div class="sub-col desc">{{ row.product_description || '-' }}</div>
|
<div class="sub-col desc">{{ row.product_description || '-' }}</div>
|
||||||
|
<div class="sub-col kategori">{{ row.kategori || '-' }}</div>
|
||||||
<div class="sub-col renk">
|
<div class="sub-col renk">
|
||||||
<div class="renk-kodu">{{ variantCode(row) }}</div>
|
<div class="renk-kodu">{{ variantCode(row) }}</div>
|
||||||
<div class="renk-aciklama">{{ row.color_title || '-' }}</div>
|
<div class="renk-aciklama">{{ row.color_title || '-' }}</div>
|
||||||
@@ -273,6 +299,7 @@ const errorMessage = ref('')
|
|||||||
const columnFilters = ref({
|
const columnFilters = ref({
|
||||||
model: [],
|
model: [],
|
||||||
desc: [],
|
desc: [],
|
||||||
|
kategori: [],
|
||||||
renk: [],
|
renk: [],
|
||||||
ana: [],
|
ana: [],
|
||||||
alt: [],
|
alt: [],
|
||||||
@@ -281,6 +308,7 @@ const columnFilters = ref({
|
|||||||
const filterSearch = ref({
|
const filterSearch = ref({
|
||||||
model: '',
|
model: '',
|
||||||
desc: '',
|
desc: '',
|
||||||
|
kategori: '',
|
||||||
renk: '',
|
renk: '',
|
||||||
ana: '',
|
ana: '',
|
||||||
alt: '',
|
alt: '',
|
||||||
@@ -325,6 +353,7 @@ const productGroups = computed(() => displayRows.value)
|
|||||||
const filterOptions = computed(() => ({
|
const filterOptions = computed(() => ({
|
||||||
model: uniqueOptions(rows.value.map(row => row.product_code)),
|
model: uniqueOptions(rows.value.map(row => row.product_code)),
|
||||||
desc: uniqueOptions(rows.value.map(row => row.product_description)),
|
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))),
|
renk: uniqueOptions(rows.value.map(row => variantCode(row))),
|
||||||
ana: uniqueOptions(rows.value.map(row => row.urun_ana_grubu)),
|
ana: uniqueOptions(rows.value.map(row => row.urun_ana_grubu)),
|
||||||
alt: uniqueOptions(rows.value.map(row => row.urun_alt_grubu)),
|
alt: uniqueOptions(rows.value.map(row => row.urun_alt_grubu)),
|
||||||
@@ -370,6 +399,7 @@ function clearFilter (key) {
|
|||||||
function rowPassesFilters (row) {
|
function rowPassesFilters (row) {
|
||||||
return filterMatch('model', row.product_code) &&
|
return filterMatch('model', row.product_code) &&
|
||||||
filterMatch('desc', row.product_description) &&
|
filterMatch('desc', row.product_description) &&
|
||||||
|
filterMatch('kategori', row.kategori) &&
|
||||||
filterMatch('renk', variantCode(row)) &&
|
filterMatch('renk', variantCode(row)) &&
|
||||||
filterMatch('ana', row.urun_ana_grubu) &&
|
filterMatch('ana', row.urun_ana_grubu) &&
|
||||||
filterMatch('alt', row.urun_alt_grubu) &&
|
filterMatch('alt', row.urun_alt_grubu) &&
|
||||||
@@ -492,7 +522,7 @@ function mapRowSizesToSchema (row) {
|
|||||||
const rawSizeLabels = sizeEntries.map(([rawSize]) => normalizeBedenLabel(rawSize))
|
const rawSizeLabels = sizeEntries.map(([rawSize]) => normalizeBedenLabel(rawSize))
|
||||||
const schemaMap = getSchemaMap()
|
const schemaMap = getSchemaMap()
|
||||||
const grpKey = detectRowGroupKey(row, rawSizeLabels)
|
const grpKey = detectRowGroupKey(row, rawSizeLabels)
|
||||||
const schema = grpKey && schemaMap?.[grpKey] ? schemaMap[grpKey] : null
|
const schema = pickSchemaForRow(schemaMap, grpKey, rawSizeLabels)
|
||||||
const schemaLabelMap = normalizedSchemaLabelMap(schema)
|
const schemaLabelMap = normalizedSchemaLabelMap(schema)
|
||||||
const mapped = {}
|
const mapped = {}
|
||||||
|
|
||||||
@@ -513,6 +543,26 @@ function mapRowSizesToSchema (row) {
|
|||||||
return { grpKey, schema, mapped }
|
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) {
|
function formatQty (value) {
|
||||||
const n = Number(value || 0)
|
const n = Number(value || 0)
|
||||||
if (!Number.isFinite(n) || n === 0) return ''
|
if (!Number.isFinite(n) || n === 0) return ''
|
||||||
@@ -544,7 +594,7 @@ function csvEscape (v) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function exportVisibleExcel () {
|
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)
|
const sizeCols = schemaRows.value.flatMap(grp => paddedSchemaValues(grp).map(v => (v.ghost ? '' : v.value))).filter(Boolean)
|
||||||
cols.push(...sizeCols)
|
cols.push(...sizeCols)
|
||||||
cols.push('TOPLAM', 'SERI', 'UYARI')
|
cols.push('TOPLAM', 'SERI', 'UYARI')
|
||||||
@@ -559,6 +609,7 @@ function exportVisibleExcel () {
|
|||||||
lines.push([
|
lines.push([
|
||||||
row.product_code || '',
|
row.product_code || '',
|
||||||
row.product_description || '',
|
row.product_description || '',
|
||||||
|
row.kategori || '',
|
||||||
variantCode(row),
|
variantCode(row),
|
||||||
row.urun_ana_grubu || '',
|
row.urun_ana_grubu || '',
|
||||||
row.urun_alt_grubu || '',
|
row.urun_alt_grubu || '',
|
||||||
@@ -590,6 +641,7 @@ onMounted(() => reload())
|
|||||||
--psq-sticky-offset: 12px;
|
--psq-sticky-offset: 12px;
|
||||||
--grp-title-w: 90px;
|
--grp-title-w: 90px;
|
||||||
--col-desc: var(--col-aciklama);
|
--col-desc: var(--col-aciklama);
|
||||||
|
--col-kategori-series: 86px;
|
||||||
--col-marka-series: var(--col-marka, 90px);
|
--col-marka-series: var(--col-marka, 90px);
|
||||||
--psq-header-h: var(--grid-header-h);
|
--psq-header-h: var(--grid-header-h);
|
||||||
--series-total-w: 76px;
|
--series-total-w: 76px;
|
||||||
@@ -606,6 +658,7 @@ onMounted(() => reload())
|
|||||||
grid-template-columns:
|
grid-template-columns:
|
||||||
var(--col-model)
|
var(--col-model)
|
||||||
var(--col-desc)
|
var(--col-desc)
|
||||||
|
var(--col-kategori-series)
|
||||||
var(--col-renk)
|
var(--col-renk)
|
||||||
var(--col-ana)
|
var(--col-ana)
|
||||||
var(--col-alt)
|
var(--col-alt)
|
||||||
@@ -699,11 +752,11 @@ onMounted(() => reload())
|
|||||||
}
|
}
|
||||||
|
|
||||||
.total-header-cell {
|
.total-header-cell {
|
||||||
grid-column: 8 / 9;
|
grid-column: 9 / 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-header-cell {
|
.series-header-cell {
|
||||||
grid-column: 9 / 10;
|
grid-column: 10 / 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-flat-row {
|
.series-flat-row {
|
||||||
@@ -741,12 +794,14 @@ onMounted(() => reload())
|
|||||||
|
|
||||||
.series-flat-row .sub-col.model { grid-column: 1; }
|
.series-flat-row .sub-col.model { grid-column: 1; }
|
||||||
.series-flat-row .sub-col.desc { grid-column: 2; }
|
.series-flat-row .sub-col.desc { grid-column: 2; }
|
||||||
.series-flat-row .sub-col.renk { grid-column: 3; }
|
.series-flat-row .sub-col.kategori { grid-column: 3; }
|
||||||
.series-flat-row .sub-col.ana { grid-column: 4; }
|
.series-flat-row .sub-col.renk { grid-column: 4; }
|
||||||
.series-flat-row .sub-col.alt { grid-column: 5; }
|
.series-flat-row .sub-col.ana { grid-column: 5; }
|
||||||
.series-flat-row .sub-col.marka { grid-column: 6; }
|
.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.model,
|
||||||
|
.series-flat-row .sub-col.kategori,
|
||||||
.series-flat-row .sub-col.renk,
|
.series-flat-row .sub-col.renk,
|
||||||
.series-flat-row .sub-col.ana,
|
.series-flat-row .sub-col.ana,
|
||||||
.series-flat-row .sub-col.alt,
|
.series-flat-row .sub-col.alt,
|
||||||
@@ -776,7 +831,7 @@ onMounted(() => reload())
|
|||||||
}
|
}
|
||||||
|
|
||||||
.flat-size-cells {
|
.flat-size-cells {
|
||||||
grid-column: 7;
|
grid-column: 8;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
grid-auto-columns: var(--beden-w);
|
grid-auto-columns: var(--beden-w);
|
||||||
@@ -818,7 +873,7 @@ onMounted(() => reload())
|
|||||||
}
|
}
|
||||||
|
|
||||||
.total-cell {
|
.total-cell {
|
||||||
grid-column: 8;
|
grid-column: 9;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -831,7 +886,7 @@ onMounted(() => reload())
|
|||||||
}
|
}
|
||||||
|
|
||||||
.series-select-cell {
|
.series-select-cell {
|
||||||
grid-column: 9 / 10;
|
grid-column: 10 / 11;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -840,7 +840,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="text-subtitle1 text-weight-bold">Fiyat Kontrolu (Satinalma Ortalama)</div>
|
<div class="text-subtitle1 text-weight-bold">Fiyat Kontrolu (Satinalma Ortalama)</div>
|
||||||
<div class="text-caption text-grey-7">
|
<div class="text-caption text-grey-7">
|
||||||
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.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-badge color="primary" outline>{{ priceDeviationRows.length }} satir</q-badge>
|
<q-badge color="primary" outline>{{ priceDeviationRows.length }} satir</q-badge>
|
||||||
@@ -1105,6 +1105,7 @@ import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
|||||||
import { usePermission } from 'src/composables/usePermission'
|
import { usePermission } from 'src/composables/usePermission'
|
||||||
import { get, post, download, extractApiErrorDetail } from 'src/services/api'
|
import { get, post, download, extractApiErrorDetail } from 'src/services/api'
|
||||||
import { createTraceId, slog } from 'src/utils/slog'
|
import { createTraceId, slog } from 'src/utils/slog'
|
||||||
|
import { formatDataSourceLabel } from 'src/utils/formatters'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -2334,8 +2335,8 @@ function resolveHistorySourceType (item, forcedType = '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveHistorySourceLabel (sourceType) {
|
function resolveHistorySourceLabel (sourceType) {
|
||||||
if (sourceType === 'purchase') return 'BAGGI_V3'
|
if (sourceType === 'purchase') return formatDataSourceLabel('BAGGI_V3')
|
||||||
if (sourceType === 'recipe') return 'URETIM'
|
if (sourceType === 'recipe') return formatDataSourceLabel('URETIM')
|
||||||
return 'HISTORY'
|
return 'HISTORY'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4770,7 +4771,7 @@ async function confirmBrPriceDeviationIfNeeded () {
|
|||||||
html: true,
|
html: true,
|
||||||
message: `
|
message: `
|
||||||
<div style="margin-bottom:10px;">
|
<div style="margin-bottom:10px;">
|
||||||
Bazı satırlarda girilen fiyat, BAGGI_V3 satınalma geçmişindeki <b>son 10</b> kaydın USD ortalamasından <b>%10</b>'dan fazla sapıyor.
|
Bazı satırlarda girilen fiyat, NEBIM_V3 satınalma geçmişindeki <b>son 10</b> kaydın USD ortalamasından <b>%10</b>'dan fazla sapıyor.
|
||||||
</div>
|
</div>
|
||||||
<div style="max-height: 360px; overflow:auto; border:1px solid #e0e0e0; border-radius:6px;">
|
<div style="max-height: 360px; overflow:auto; border:1px solid #e0e0e0; border-radius:6px;">
|
||||||
<table style="width:100%; border-collapse:collapse; font-size:13px;">
|
<table style="width:100%; border-collapse:collapse; font-size:13px;">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="text-h6">Maliyet Parca Eslestirme</div>
|
<div class="text-h6">Maliyet Parca Eslestirme</div>
|
||||||
<div class="text-caption text-grey-7">
|
<div class="text-caption text-grey-7">
|
||||||
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
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|||||||
import { useQuasar } from 'quasar'
|
import { useQuasar } from 'quasar'
|
||||||
import { usePermission } from 'src/composables/usePermission'
|
import { usePermission } from 'src/composables/usePermission'
|
||||||
import { useTranslationStore } from 'src/stores/translationStore'
|
import { useTranslationStore } from 'src/stores/translationStore'
|
||||||
|
import { formatDataSourceLabel } from 'src/utils/formatters'
|
||||||
|
|
||||||
const $q = useQuasar()
|
const $q = useQuasar()
|
||||||
const store = useTranslationStore()
|
const store = useTranslationStore()
|
||||||
@@ -231,15 +232,10 @@ const tablePagination = ref({
|
|||||||
let filterReloadTimer = null
|
let filterReloadTimer = null
|
||||||
|
|
||||||
const sourceTypeOptions = [
|
const sourceTypeOptions = [
|
||||||
{ label: 'dummy', value: 'dummy' },
|
{ label: formatDataSourceLabel('dummy'), value: 'dummy' },
|
||||||
{ label: 'postgre', value: 'postgre' },
|
{ label: formatDataSourceLabel('postgre'), value: 'postgre' },
|
||||||
{ label: 'mssql', value: 'mssql' }
|
{ label: formatDataSourceLabel('mssql'), value: 'mssql' }
|
||||||
]
|
]
|
||||||
const sourceTypeLabelMap = {
|
|
||||||
dummy: 'UI',
|
|
||||||
postgre: 'PostgreSQL',
|
|
||||||
mssql: 'MSSQL'
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ name: 'actions', label: 'Güncelle', field: 'actions', align: 'left' },
|
{ name: 'actions', label: 'Güncelle', field: 'actions', align: 'left' },
|
||||||
@@ -413,7 +409,7 @@ function cellClass (key, field) {
|
|||||||
|
|
||||||
function sourceTypeLabel (key) {
|
function sourceTypeLabel (key) {
|
||||||
const val = String(rowDraft(key).source_type || 'dummy').toLowerCase()
|
const val = String(rowDraft(key).source_type || 'dummy').toLowerCase()
|
||||||
return sourceTypeLabelMap[val] || val || '-'
|
return formatDataSourceLabel(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSelected (key, checked) {
|
function toggleSelected (key, checked) {
|
||||||
|
|||||||
@@ -3,9 +3,8 @@
|
|||||||
v-if="canReadUser"
|
v-if="canReadUser"
|
||||||
class="q-pa-md user-sync-page"
|
class="q-pa-md user-sync-page"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div class="row items-center justify-between q-mb-md">
|
<div class="row items-center justify-between q-mb-md">
|
||||||
<div class="text-h6 text-primary">👤 Kullanıcı Yönetimi</div>
|
<div class="text-h6 text-primary">Kullanici Yonetimi</div>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="canUpdateUser"
|
v-if="canUpdateUser"
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -20,11 +19,10 @@
|
|||||||
<q-separator />
|
<q-separator />
|
||||||
|
|
||||||
<div class="row q-col-gutter-md q-mt-md">
|
<div class="row q-col-gutter-md q-mt-md">
|
||||||
<!-- 🔹 PostgreSQL Kullanıcıları -->
|
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<q-card flat bordered>
|
<q-card flat bordered>
|
||||||
<q-card-section class="bg-primary text-white text-subtitle1">
|
<q-card-section class="bg-primary text-white text-subtitle1">
|
||||||
PostgreSQL Kullanıcıları
|
B2B Kullanicilari
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-table
|
<q-table
|
||||||
@@ -36,7 +34,7 @@
|
|||||||
bordered
|
bordered
|
||||||
separator="cell"
|
separator="cell"
|
||||||
>
|
>
|
||||||
<template v-slot:body-cell-sync_status="props">
|
<template #body-cell-sync_status="props">
|
||||||
<q-td :props="props">
|
<q-td :props="props">
|
||||||
<q-chip
|
<q-chip
|
||||||
:color="statusColor(props.row.sync_status)"
|
:color="statusColor(props.row.sync_status)"
|
||||||
@@ -48,23 +46,27 @@
|
|||||||
</q-td>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-slot:body-cell-actions="props">
|
<template #body-cell-actions="props">
|
||||||
<q-td :props="props">
|
<q-td :props="props">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="canUpdateUser"
|
v-if="canUpdateUser"
|
||||||
dense flat icon="link"
|
dense
|
||||||
|
flat
|
||||||
|
icon="link"
|
||||||
color="primary"
|
color="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
@click="openMapDialog(props.row)"
|
|
||||||
:disable="store.loading || !canUpdateUser"
|
:disable="store.loading || !canUpdateUser"
|
||||||
|
@click="openMapDialog(props.row)"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="canUpdateUser"
|
v-if="canUpdateUser"
|
||||||
dense flat icon="link_off"
|
dense
|
||||||
|
flat
|
||||||
|
icon="link_off"
|
||||||
color="negative"
|
color="negative"
|
||||||
size="sm"
|
size="sm"
|
||||||
@click="store.unmap(props.row.id)"
|
|
||||||
:disable="store.loading || !props.row.mssql_username || !canUpdateUser"
|
:disable="store.loading || !props.row.mssql_username || !canUpdateUser"
|
||||||
|
@click="store.unmap(props.row.id)"
|
||||||
/>
|
/>
|
||||||
</q-td>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
@@ -72,11 +74,10 @@
|
|||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 🔸 MSSQL Kullanıcıları -->
|
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<q-card flat bordered>
|
<q-card flat bordered>
|
||||||
<q-card-section class="bg-secondary text-white text-subtitle1">
|
<q-card-section class="bg-secondary text-white text-subtitle1">
|
||||||
MSSQL Kullanıcıları
|
NEBIM_V3 Kullanicilari
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-table
|
<q-table
|
||||||
@@ -88,7 +89,7 @@
|
|||||||
bordered
|
bordered
|
||||||
separator="cell"
|
separator="cell"
|
||||||
>
|
>
|
||||||
<template v-slot:body-cell-is_blocked="props">
|
<template #body-cell-is_blocked="props">
|
||||||
<q-td :props="props">
|
<q-td :props="props">
|
||||||
<q-chip
|
<q-chip
|
||||||
:color="props.row.is_blocked ? 'negative' : 'positive'"
|
:color="props.row.is_blocked ? 'negative' : 'positive'"
|
||||||
@@ -117,9 +118,9 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import { useUserSyncStore } from 'src/stores/userSyncStore'
|
|
||||||
import { Dialog } from 'quasar'
|
import { Dialog } from 'quasar'
|
||||||
import { usePermission } from 'src/composables/usePermission'
|
import { usePermission } from 'src/composables/usePermission'
|
||||||
|
import { useUserSyncStore } from 'src/stores/userSyncStore'
|
||||||
|
|
||||||
const { canRead, canUpdate } = usePermission()
|
const { canRead, canUpdate } = usePermission()
|
||||||
const canReadUser = canRead('user')
|
const canReadUser = canRead('user')
|
||||||
@@ -132,13 +133,13 @@ const pgColumns = [
|
|||||||
{ name: 'code', label: 'Kodu', field: 'code', align: 'left' },
|
{ name: 'code', label: 'Kodu', field: 'code', align: 'left' },
|
||||||
{ name: 'full_name', label: 'Ad Soyad', field: 'full_name', align: 'left' },
|
{ name: 'full_name', label: 'Ad Soyad', field: 'full_name', align: 'left' },
|
||||||
{ name: 'email', label: 'E-posta', field: 'email', align: 'left' },
|
{ name: 'email', label: 'E-posta', field: 'email', align: 'left' },
|
||||||
{ name: 'mssql_username', label: 'MSSQL Kullanıcı', field: 'mssql_username', align: 'left' },
|
{ name: 'mssql_username', label: 'NEBIM_V3 Kullanicisi', field: 'mssql_username', align: 'left' },
|
||||||
{ name: 'sync_status', label: 'Durum', field: 'sync_status', align: 'center' },
|
{ name: 'sync_status', label: 'Durum', field: 'sync_status', align: 'center' },
|
||||||
{ name: 'actions', label: 'İşlemler', field: 'actions', align: 'center' }
|
{ name: 'actions', label: 'Islemler', field: 'actions', align: 'center' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const msColumns = [
|
const msColumns = [
|
||||||
{ name: 'username', label: 'Kullanıcı Adı', field: 'username', align: 'left' },
|
{ name: 'username', label: 'Kullanici Adi', field: 'username', align: 'left' },
|
||||||
{ name: 'first_name', label: 'Ad', field: 'first_name', align: 'left' },
|
{ name: 'first_name', label: 'Ad', field: 'first_name', align: 'left' },
|
||||||
{ name: 'last_name', label: 'Soyad', field: 'last_name', align: 'left' },
|
{ name: 'last_name', label: 'Soyad', field: 'last_name', align: 'left' },
|
||||||
{ name: 'email', label: 'E-posta', field: 'email', align: 'left' },
|
{ name: 'email', label: 'E-posta', field: 'email', align: 'left' },
|
||||||
@@ -156,13 +157,11 @@ function statusColor(status) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openMapDialog (pgUser) {
|
function openMapDialog (pgUser) {
|
||||||
if (!canUpdateUser.value) {
|
if (!canUpdateUser.value) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Dialog.create({
|
Dialog.create({
|
||||||
title: 'Kullanıcı Eşleme',
|
title: 'Kullanici Esleme',
|
||||||
message: 'Bu PostgreSQL kullanıcısını hangi MSSQL kullanıcısına bağlamak istiyorsunuz?',
|
message: 'Bu B2B kullanicisini hangi NEBIM_V3 kullanicisina baglamak istiyorsunuz?',
|
||||||
options: {
|
options: {
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
model: '',
|
model: '',
|
||||||
@@ -186,6 +185,7 @@ onMounted(() => {
|
|||||||
.user-sync-page {
|
.user-sync-page {
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.q-card-section {
|
.q-card-section {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import qs from 'qs'
|
|||||||
import { useAuthStore } from 'stores/authStore'
|
import { useAuthStore } from 'stores/authStore'
|
||||||
import { DEFAULT_LOCALE, normalizeLocale } from 'src/i18n/languages'
|
import { DEFAULT_LOCALE, normalizeLocale } from 'src/i18n/languages'
|
||||||
import { slog } from 'src/utils/slog'
|
import { slog } from 'src/utils/slog'
|
||||||
|
import { formatDataSourceLabel } from 'src/utils/formatters'
|
||||||
|
|
||||||
const rawBaseUrl =
|
const rawBaseUrl =
|
||||||
(typeof process !== 'undefined' && process.env?.VITE_API_BASE_URL) || '/api'
|
(typeof process !== 'undefined' && process.env?.VITE_API_BASE_URL) || '/api'
|
||||||
@@ -63,7 +64,7 @@ function sanitizeApiErrorDetail(detail, status) {
|
|||||||
return `Unexpected HTML error response (${status || '-'})`
|
return `Unexpected HTML error response (${status || '-'})`
|
||||||
}
|
}
|
||||||
|
|
||||||
const compact = normalized.replace(/\s+/g, ' ').trim()
|
const compact = normalizeTechnicalSourceLabels(normalized.replace(/\s+/g, ' ').trim())
|
||||||
if (compact.length > 320) {
|
if (compact.length > 320) {
|
||||||
return `${compact.slice(0, 320)}...`
|
return `${compact.slice(0, 320)}...`
|
||||||
}
|
}
|
||||||
@@ -71,6 +72,17 @@ function sanitizeApiErrorDetail(detail, status) {
|
|||||||
return compact
|
return compact
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeTechnicalSourceLabels(text) {
|
||||||
|
return String(text || '')
|
||||||
|
.replace(/\bPostgreSQL\b/gi, formatDataSourceLabel('postgresql'))
|
||||||
|
.replace(/\bPostgres\b/gi, formatDataSourceLabel('postgres'))
|
||||||
|
.replace(/\bpostgre\b/gi, formatDataSourceLabel('postgre'))
|
||||||
|
.replace(/\bPG\b/g, formatDataSourceLabel('pg'))
|
||||||
|
.replace(/\bMSSQL\b/gi, formatDataSourceLabel('mssql'))
|
||||||
|
.replace(/\bBAGGI_V3\b/gi, formatDataSourceLabel('BAGGI_V3'))
|
||||||
|
.replace(/\bURETIM\b/g, formatDataSourceLabel('URETIM'))
|
||||||
|
}
|
||||||
|
|
||||||
function redirectToLogin() {
|
function redirectToLogin() {
|
||||||
if (typeof window === 'undefined') return
|
if (typeof window === 'undefined') return
|
||||||
if (window.location.hash === '#/login') return
|
if (window.location.hash === '#/login') return
|
||||||
@@ -203,6 +215,8 @@ api.interceptors.response.use(
|
|||||||
requestUrl.startsWith('/me/password')
|
requestUrl.startsWith('/me/password')
|
||||||
const isPublicRequest = isPublicPath(requestUrl)
|
const isPublicRequest = isPublicPath(requestUrl)
|
||||||
|
|
||||||
|
normalizeErrorResponsePayload(error)
|
||||||
|
|
||||||
if ((status >= 500 || hasBlob) && error) {
|
if ((status >= 500 || hasBlob) && error) {
|
||||||
const method = String(requestConfig.method || 'GET').toUpperCase()
|
const method = String(requestConfig.method || 'GET').toUpperCase()
|
||||||
const detail = sanitizeApiErrorDetail(
|
const detail = sanitizeApiErrorDetail(
|
||||||
@@ -262,6 +276,22 @@ api.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function normalizeErrorResponsePayload(error) {
|
||||||
|
const data = error?.response?.data
|
||||||
|
if (!data || typeof Blob !== 'undefined' && data instanceof Blob) return
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
error.response.data = normalizeTechnicalSourceLabels(data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof data === 'object') {
|
||||||
|
for (const key of ['detail', 'message', 'error']) {
|
||||||
|
if (typeof data[key] === 'string') {
|
||||||
|
data[key] = normalizeTechnicalSourceLabels(data[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const get = (u, p = {}, c = {}) =>
|
export const get = (u, p = {}, c = {}) =>
|
||||||
api.get(u, { params: p, ...c }).then(r => r.data)
|
api.get(u, { params: p, ...c }).then(r => r.data)
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,13 @@ const SIZE_GROUP_TITLES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FALLBACK_SCHEMA_MAP = {
|
const FALLBACK_SCHEMA_MAP = {
|
||||||
tak: { key: 'tak', title: 'TAKIM ELBISE', values: ['44', '46', '48', '50', '52', '54', '56', '58', '60', '62', '64', '66', '68', '70', '72', '74'] }
|
tak: { key: 'tak', title: 'TAKIM ELBISE', values: ['44', '46', '48', '50', '52', '54', '56', '58', '60', '62', '64', '66', '68', '70', '72', '74'] },
|
||||||
|
ayk: { key: 'ayk', title: 'AYAKKABI', values: ['39', '40', '41', '42', '43', '44', '45'] },
|
||||||
|
ayk_garson: { key: 'ayk_garson', title: 'AYAKKABI GARSON', values: ['31', '32', '33', '34', '35', '36', '37', '38'] },
|
||||||
|
yas: { key: 'yas', title: 'YAS', values: ['2', '4', '6', '8', '10', '12', '14'] },
|
||||||
|
pan: { key: 'pan', title: 'PANTOLON', values: ['38', '40', '42', '44', '46', '48', '50', '52', '54', '56', '58', '60'] },
|
||||||
|
gom: { key: 'gom', title: 'GOMLEK', values: ['XS', 'S', 'M', 'L', 'XL', '2XL', '3XL', '4XL', '5XL', '6XL', '7XL'] },
|
||||||
|
aksbir: { key: 'aksbir', title: 'AKSESUAR', values: [' '] }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const schemaByKey = { ...FALLBACK_SCHEMA_MAP }
|
export const schemaByKey = { ...FALLBACK_SCHEMA_MAP }
|
||||||
|
|||||||
91
ui/src/stores/runtimeTranslationStore.js
Normal file
91
ui/src/stores/runtimeTranslationStore.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import api from 'src/services/api'
|
||||||
|
import { DEFAULT_LOCALE, normalizeLocale } from 'src/i18n/languages'
|
||||||
|
|
||||||
|
function normalizeRuntimeText (value) {
|
||||||
|
return String(value || '').trim().replace(/\s+/g, ' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRuntimeTranslationStore = defineStore('runtimeTranslation', () => {
|
||||||
|
const locale = ref(DEFAULT_LOCALE)
|
||||||
|
const byKey = ref({})
|
||||||
|
const byText = ref({})
|
||||||
|
const loading = ref(false)
|
||||||
|
const loadedLocales = ref({})
|
||||||
|
const version = ref(0)
|
||||||
|
|
||||||
|
async function loadLocale (nextLocale, options = {}) {
|
||||||
|
const normalized = normalizeLocale(nextLocale)
|
||||||
|
const force = Boolean(options?.force)
|
||||||
|
locale.value = normalized
|
||||||
|
|
||||||
|
if (!force && loadedLocales.value[normalized]) {
|
||||||
|
byKey.value = loadedLocales.value[normalized].byKey
|
||||||
|
byText.value = loadedLocales.value[normalized].byText
|
||||||
|
version.value += 1
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await api.get('/language/translations/runtime', {
|
||||||
|
params: { lang: normalized },
|
||||||
|
timeout: 180000
|
||||||
|
})
|
||||||
|
const payload = res?.data || {}
|
||||||
|
const nextByKey = payload?.by_key && typeof payload.by_key === 'object' ? payload.by_key : {}
|
||||||
|
const nextByTextRaw = payload?.by_text && typeof payload.by_text === 'object' ? payload.by_text : {}
|
||||||
|
const nextByText = {}
|
||||||
|
for (const [source, translated] of Object.entries(nextByTextRaw)) {
|
||||||
|
const sourceKey = normalizeRuntimeText(source)
|
||||||
|
const display = normalizeRuntimeText(translated)
|
||||||
|
if (sourceKey && display) nextByText[sourceKey] = display
|
||||||
|
}
|
||||||
|
|
||||||
|
loadedLocales.value = {
|
||||||
|
...loadedLocales.value,
|
||||||
|
[normalized]: {
|
||||||
|
byKey: nextByKey,
|
||||||
|
byText: nextByText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
byKey.value = nextByKey
|
||||||
|
byText.value = nextByText
|
||||||
|
version.value += 1
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[runtime-i18n] dictionary load failed', normalized, err)
|
||||||
|
byKey.value = {}
|
||||||
|
byText.value = {}
|
||||||
|
version.value += 1
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function translateKey (key) {
|
||||||
|
const raw = String(key || '').trim()
|
||||||
|
if (!raw) return ''
|
||||||
|
return byKey.value[raw] || raw
|
||||||
|
}
|
||||||
|
|
||||||
|
function translateText (text) {
|
||||||
|
const normalized = normalizeRuntimeText(text)
|
||||||
|
if (!normalized) return ''
|
||||||
|
return byText.value[normalized] || normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
locale,
|
||||||
|
byKey,
|
||||||
|
byText,
|
||||||
|
loading,
|
||||||
|
version,
|
||||||
|
loadLocale,
|
||||||
|
translateKey,
|
||||||
|
translateText
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -84,3 +84,13 @@ export function formatPercent(value) {
|
|||||||
const n = Number(value || 0)
|
const n = Number(value || 0)
|
||||||
return `${(n * 100).toFixed(2)}%`
|
return `${(n * 100).toFixed(2)}%`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatDataSourceLabel(value) {
|
||||||
|
const raw = String(value || '').trim()
|
||||||
|
const normalized = raw.toLowerCase().replace(/[\s-]+/g, '_')
|
||||||
|
if (['pg', 'postgre', 'postgres', 'postgresql'].includes(normalized)) return 'B2B'
|
||||||
|
if (['mssql', 'baggi_v3', 'baggi'].includes(normalized)) return 'NEBIM_V3'
|
||||||
|
if (normalized === 'uretim') return 'TEDARIK URETIM PROGRAMI'
|
||||||
|
if (normalized === 'dummy' || normalized === 'ui') return 'UI'
|
||||||
|
return raw || '-'
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user